瀏覽代碼

Recognize file changes and remove data on change (#13839)

Tim Eisele 4 周之前
父節點
當前提交
d976f13970
共有 57 個文件被更改,包括 1914 次插入1124 次删除
  1. 2 2
      Emby.Server.Implementations/ApplicationHost.cs
  2. 4 4
      Emby.Server.Implementations/Collections/CollectionManager.cs
  3. 44 0
      Emby.Server.Implementations/Library/KeyframeManager.cs
  4. 77 45
      Emby.Server.Implementations/Library/LibraryManager.cs
  5. 14 0
      Emby.Server.Implementations/Library/PathManager.cs
  6. 15 5
      Emby.Server.Implementations/Library/ResolverHelper.cs
  7. 4 2
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  8. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
  9. 1 1
      Jellyfin.Api/Controllers/MediaSegmentsController.cs
  10. 8 0
      Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
  11. 8 1
      Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
  12. 5 0
      Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
  13. 5 0
      Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
  14. 131 0
      Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
  15. 36 4
      MediaBrowser.Controller/Entities/BaseItem.cs
  16. 9 1
      MediaBrowser.Controller/IO/IPathManager.cs
  17. 37 0
      MediaBrowser.Controller/Library/IKeyframeManager.cs
  18. 4 4
      MediaBrowser.Controller/Library/ILibraryManager.cs
  19. 8 1
      MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
  20. 2 4
      MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
  21. 8 0
      MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
  22. 2 2
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  23. 48 32
      MediaBrowser.Providers/Books/AudioBookMetadataService.cs
  24. 37 21
      MediaBrowser.Providers/Books/BookMetadataService.cs
  25. 70 54
      MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
  26. 29 13
      MediaBrowser.Providers/Channels/ChannelMetadataService.cs
  27. 29 13
      MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
  28. 33 17
      MediaBrowser.Providers/Folders/FolderMetadataService.cs
  29. 29 13
      MediaBrowser.Providers/Folders/UserViewMetadataService.cs
  30. 29 13
      MediaBrowser.Providers/Genres/GenreMetadataService.cs
  31. 29 13
      MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
  32. 75 1
      MediaBrowser.Providers/Manager/MetadataService.cs
  33. 1 2
      MediaBrowser.Providers/Manager/ProviderManager.cs
  34. 0 1
      MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
  35. 10 6
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  36. 2 2
      MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
  37. 0 1
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  38. 39 23
      MediaBrowser.Providers/Movies/MovieMetadataService.cs
  39. 40 24
      MediaBrowser.Providers/Movies/TrailerMetadataService.cs
  40. 183 176
      MediaBrowser.Providers/Music/AlbumMetadataService.cs
  41. 42 27
      MediaBrowser.Providers/Music/ArtistMetadataService.cs
  42. 59 52
      MediaBrowser.Providers/Music/AudioMetadataService.cs
  43. 52 36
      MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
  44. 29 13
      MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
  45. 29 13
      MediaBrowser.Providers/People/PersonMetadataService.cs
  46. 29 13
      MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
  47. 29 13
      MediaBrowser.Providers/Photos/PhotoMetadataService.cs
  48. 63 47
      MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
  49. 29 13
      MediaBrowser.Providers/Studios/StudioMetadataService.cs
  50. 83 81
      MediaBrowser.Providers/TV/EpisodeMetadataService.cs
  51. 81 74
      MediaBrowser.Providers/TV/SeasonMetadataService.cs
  52. 213 206
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  53. 33 17
      MediaBrowser.Providers/Videos/VideoMetadataService.cs
  54. 29 13
      MediaBrowser.Providers/Years/YearMetadataService.cs
  55. 2 2
      src/Jellyfin.Drawing/ImageProcessor.cs
  56. 3 2
      src/Jellyfin.LiveTv/Channels/ChannelManager.cs
  57. 1 0
      tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs

+ 2 - 2
Emby.Server.Implementations/ApplicationHost.cs

@@ -36,7 +36,6 @@ using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Jellyfin.Api.Helpers;
-using Jellyfin.Database.Implementations;
 using Jellyfin.Drawing;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.Networking.Manager;
@@ -63,6 +62,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
@@ -93,7 +93,6 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -560,6 +559,7 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
             serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
+            serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
 
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 

+ 4 - 4
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
 
             var libraryOptions = new LibraryOptions
             {
-                PathInfos = new[] { new MediaPathInfo(path) },
+                PathInfos = [new MediaPathInfo(path)],
                 EnableRealtimeMonitor = false,
                 SaveLocalMetadata = true
             };
@@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
 
             try
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 var collection = new BoxSet
                 {
                     Name = name,
                     Path = path,
                     IsLocked = options.IsLocked,
                     ProviderIds = options.ProviderIds,
-                    DateCreated = DateTime.UtcNow
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc
                 };
 
                 parentFolder.AddChild(collection);

+ 44 - 0
Emby.Server.Implementations/Library/KeyframeManager.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Manager for Keyframe data.
+/// </summary>
+public class KeyframeManager : IKeyframeManager
+{
+    private readonly IKeyframeRepository _repository;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="KeyframeManager"/> class.
+    /// </summary>
+    /// <param name="repository">The keyframe repository.</param>
+    public KeyframeManager(IKeyframeRepository repository)
+    {
+        _repository = repository;
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
+    {
+        return _repository.GetKeyframeData(itemId);
+    }
+
+    /// <inheritdoc />
+    public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
+    {
+        await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
+    }
+
+    /// <inheritdoc />
+    public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+    {
+        await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+    }
+}

+ 77 - 45
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -208,7 +208,7 @@ namespace Emby.Server.Implementations.Library
         /// Gets or sets the postscan tasks.
         /// </summary>
         /// <value>The postscan tasks.</value>
-        private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
+        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 
         /// <summary>
         /// Gets or sets the intro providers.
@@ -245,20 +245,20 @@ namespace Emby.Server.Implementations.Library
         /// <param name="resolvers">The resolvers.</param>
         /// <param name="introProviders">The intro providers.</param>
         /// <param name="itemComparers">The item comparers.</param>
-        /// <param name="postscanTasks">The post scan tasks.</param>
+        /// <param name="postScanTasks">The post scan tasks.</param>
         public void AddParts(
             IEnumerable<IResolverIgnoreRule> rules,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IIntroProvider> introProviders,
             IEnumerable<IBaseItemComparer> itemComparers,
-            IEnumerable<ILibraryPostScanTask> postscanTasks)
+            IEnumerable<ILibraryPostScanTask> postScanTasks)
         {
             EntityResolutionIgnoreRules = rules.ToArray();
             EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
             MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
             IntroProviders = introProviders.ToArray();
             Comparers = itemComparers.ToArray();
-            PostscanTasks = postscanTasks.ToArray();
+            PostScanTasks = postScanTasks.ToArray();
         }
 
         /// <summary>
@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            if (options.DeleteFileLocation && item.IsFileProtocol)
+            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
             {
                 // Assume only the first is required
                 // Add this flag to GetDeletePaths if required in the future
@@ -472,6 +472,36 @@ namespace Emby.Server.Implementations.Library
             ReportItemRemoved(item, parent);
         }
 
+        private bool IsInternalItem(BaseItem item)
+        {
+            if (!item.IsFileProtocol)
+            {
+                return false;
+            }
+
+            var pathToCheck = item switch
+            {
+                Genre => _configurationManager.ApplicationPaths.GenrePath,
+                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
+                MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
+                Person => _configurationManager.ApplicationPaths.PeoplePath,
+                Studio => _configurationManager.ApplicationPaths.StudioPath,
+                Year => _configurationManager.ApplicationPaths.YearPath,
+                _ => null
+            };
+
+            var itemPath = item.Path;
+            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
+            {
+                var cleanPath = _fileSystem.GetValidFilename(itemPath);
+                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
+
+                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
+            }
+
+            return false;
+        }
+
         private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
         {
             var list = GetInternalMetadataPaths(item);
@@ -639,7 +669,7 @@ namespace Emby.Server.Implementations.Library
                     }
                 }
 
-                // Need to remove subpaths that may have been resolved from shortcuts
+                // Need to remove sub-paths that may have been resolved from shortcuts
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 if (isPhysicalRoot)
                 {
@@ -772,11 +802,12 @@ namespace Emby.Server.Implementations.Library
             // Add in the plug-in folders
             var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 
-            Directory.CreateDirectory(path);
-
+            var info = Directory.CreateDirectory(path);
             Folder folder = new PlaylistsFolder
             {
-                Path = path
+                Path = path,
+                DateCreated = info.CreationTimeUtc,
+                DateModified = info.LastWriteTimeUtc,
             };
 
             if (folder.Id.IsEmpty())
@@ -862,7 +893,7 @@ namespace Emby.Server.Implementations.Library
             {
                 Path = path,
                 IsFolder = isFolder,
-                OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
                 Limit = 1,
                 DtoOptions = new DtoOptions(true)
             };
@@ -968,7 +999,7 @@ namespace Emby.Server.Implementations.Library
             {
                 var existing = GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
+                    IncludeItemTypes = [BaseItemKind.MusicArtist],
                     Name = name,
                     DtoOptions = options
                 }).Cast<MusicArtist>()
@@ -987,12 +1018,13 @@ namespace Emby.Server.Implementations.Library
             var item = GetItemById(id) as T;
             if (item is null)
             {
+                var info = Directory.CreateDirectory(path);
                 item = new T
                 {
                     Name = name,
                     Id = id,
-                    DateCreated = DateTime.UtcNow,
-                    DateModified = DateTime.UtcNow,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc,
                     Path = path
                 };
 
@@ -1118,7 +1150,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task.</returns>
         private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
         {
-            var tasks = PostscanTasks.ToList();
+            var tasks = PostScanTasks.ToList();
 
             var numComplete = 0;
             var numTasks = tasks.Count;
@@ -1241,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
 
         private CollectionTypeOptions? GetCollectionType(string path)
         {
-            var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
             foreach (ReadOnlySpan<char> file in files)
             {
                 if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
@@ -1312,7 +1344,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new[] { parent });
+                    SetTopParentIdsOrAncestors(query, [parent]);
                 }
             }
 
@@ -1343,7 +1375,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new[] { parent });
+                    SetTopParentIdsOrAncestors(query, [parent]);
                 }
             }
 
@@ -1531,7 +1563,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new[] { parent });
+                    SetTopParentIdsOrAncestors(query, [parent]);
                 }
             }
 
@@ -1561,7 +1593,7 @@ namespace Emby.Server.Implementations.Library
                 // Prevent searching in all libraries due to empty filter
                 if (query.TopParentIds.Length == 0)
                 {
-                    query.TopParentIds = new[] { Guid.NewGuid() };
+                    query.TopParentIds = [Guid.NewGuid()];
                 }
             }
             else
@@ -1572,7 +1604,7 @@ namespace Emby.Server.Implementations.Library
                 // Prevent searching in all libraries due to empty filter
                 if (query.AncestorIds.Length == 0)
                 {
-                    query.AncestorIds = new[] { Guid.NewGuid() };
+                    query.AncestorIds = [Guid.NewGuid()];
                 }
             }
 
@@ -1601,7 +1633,7 @@ namespace Emby.Server.Implementations.Library
                 // Prevent searching in all libraries due to empty filter
                 if (query.TopParentIds.Length == 0)
                 {
-                    query.TopParentIds = new[] { Guid.NewGuid() };
+                    query.TopParentIds = [Guid.NewGuid()];
                 }
             }
         }
@@ -1612,7 +1644,7 @@ namespace Emby.Server.Implementations.Library
             {
                 if (view.ViewType == CollectionType.livetv)
                 {
-                    return new[] { view.Id };
+                    return [view.Id];
                 }
 
                 // Translate view into folders
@@ -1661,7 +1693,7 @@ namespace Emby.Server.Implementations.Library
             var topParent = item.GetTopParent();
             if (topParent is not null)
             {
-                return new[] { topParent.Id };
+                return [topParent.Id];
             }
 
             return [];
@@ -1868,7 +1900,7 @@ namespace Emby.Server.Implementations.Library
         /// <inheritdoc />
         public void CreateItem(BaseItem item, BaseItem? parent)
         {
-            CreateItems(new[] { item }, parent, CancellationToken.None);
+            CreateItems([item], parent, CancellationToken.None);
         }
 
         /// <inheritdoc />
@@ -2054,7 +2086,7 @@ namespace Emby.Server.Implementations.Library
 
         /// <inheritdoc />
         public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
-            => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
+            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 
         public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
         {
@@ -2283,13 +2315,13 @@ namespace Emby.Server.Implementations.Library
 
             if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 item = new UserView
                 {
                     Path = path,
                     Id = id,
-                    DateCreated = DateTime.UtcNow,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc,
                     Name = name,
                     ViewType = viewType,
                     ForcedSortName = sortName
@@ -2331,13 +2363,13 @@ namespace Emby.Server.Implementations.Library
 
             if (item is null)
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 item = new UserView
                 {
                     Path = path,
                     Id = id,
-                    DateCreated = DateTime.UtcNow,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc,
                     Name = name,
                     ViewType = viewType,
                     ForcedSortName = sortName,
@@ -2395,20 +2427,19 @@ namespace Emby.Server.Implementations.Library
 
             if (item is null)
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 item = new UserView
                 {
                     Path = path,
                     Id = id,
-                    DateCreated = DateTime.UtcNow,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc,
                     Name = name,
                     ViewType = viewType,
-                    ForcedSortName = sortName
+                    ForcedSortName = sortName,
+                    DisplayParentId = parentId
                 };
 
-                item.DisplayParentId = parentId;
-
                 CreateItem(item, null);
 
                 isNew = true;
@@ -2465,20 +2496,19 @@ namespace Emby.Server.Implementations.Library
 
             if (item is null)
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 item = new UserView
                 {
                     Path = path,
                     Id = id,
-                    DateCreated = DateTime.UtcNow,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc,
                     Name = name,
                     ViewType = viewType,
-                    ForcedSortName = sortName
+                    ForcedSortName = sortName,
+                    DisplayParentId = parentId
                 };
 
-                item.DisplayParentId = parentId;
-
                 CreateItem(item, null);
 
                 isNew = true;
@@ -2989,12 +3019,14 @@ namespace Emby.Server.Implementations.Library
                 if (personEntity is null)
                 {
                     var path = Person.GetPath(person.Name);
+                    var info = Directory.CreateDirectory(path);
+                    var lastWriteTime = info.LastWriteTimeUtc;
                     personEntity = new Person()
                     {
                         Name = person.Name,
                         Id = GetItemByNameId<Person>(path),
-                        DateCreated = DateTime.UtcNow,
-                        DateModified = DateTime.UtcNow,
+                        DateCreated = info.CreationTimeUtc,
+                        DateModified = lastWriteTime,
                         Path = path
                     };
 

+ 14 - 0
Emby.Server.Implementations/Library/PathManager.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using MediaBrowser.Common.Configuration;
@@ -84,4 +85,17 @@ public class PathManager : IPathManager
 
         return Path.Combine(GetChapterImageFolderPath(item), filename);
     }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
+    {
+        var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
+        return [
+            GetAttachmentFolderPath(mediaSourceId),
+            GetSubtitleFolderPath(mediaSourceId),
+            GetTrickplayDirectory(item, false),
+            GetTrickplayDirectory(item, true),
+            GetChapterImageFolderPath(item)
+        ];
+    }
 }

+ 15 - 5
Emby.Server.Implementations/Library/ResolverHelper.cs

@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
 
             if (config.UseFileCreationTimeForDateAdded)
             {
-                // directoryService.getFile may return null
-                if (info is not null)
+                var fileCreationDate = info?.CreationTimeUtc;
+                if (fileCreationDate is not null)
                 {
-                    var dateCreated = info.CreationTimeUtc;
-
+                    var dateCreated = fileCreationDate;
                     if (dateCreated.Equals(DateTime.MinValue))
                     {
                         dateCreated = DateTime.UtcNow;
                     }
 
-                    item.DateCreated = dateCreated;
+                    item.DateCreated = dateCreated.Value;
                 }
             }
             else
             {
                 item.DateCreated = DateTime.UtcNow;
             }
+
+            if (info is not null && !info.IsDirectory)
+            {
+                item.Size = info.Length;
+            }
+
+            var fileModificationDate = info?.LastWriteTimeUtc;
+            if (fileModificationDate.HasValue)
+            {
+                item.DateModified = fileModificationDate.Value;
+            }
         }
     }
 }

+ 4 - 2
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists
 
             try
             {
-                Directory.CreateDirectory(path);
+                var info = Directory.CreateDirectory(path);
                 var playlist = new Playlist
                 {
                     Name = name,
                     Path = path,
                     OwnerUserId = request.UserId,
                     Shares = request.Users ?? [],
-                    OpenAccess = request.Public ?? false
+                    OpenAccess = request.Public ?? false,
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc
                 };
 
                 playlist.SetMediaType(request.MediaType);

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs

@@ -4,10 +4,10 @@ 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.Controller.MediaSegments;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Tasks;
 

+ 1 - 1
Jellyfin.Api/Controllers/MediaSegmentsController.cs

@@ -5,9 +5,9 @@ using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Database.Implementations.Enums;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Model.MediaSegments;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;

+ 8 - 0
Jellyfin.Server.Implementations/Item/KeyframeRepository.cs

@@ -61,4 +61,12 @@ public class KeyframeRepository : IKeyframeRepository
         await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
         await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
     }
+
+    /// <inheritdoc />
+    public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+    {
+        using var context = _dbProvider.CreateDbContext();
+        await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+        await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+    }
 }

+ 8 - 1
Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs

@@ -10,10 +10,10 @@ using Jellyfin.Database.Implementations.Entities;
 using Jellyfin.Database.Implementations.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.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model;
 using MediaBrowser.Model.MediaSegments;
@@ -139,6 +139,13 @@ public class MediaSegmentManager : IMediaSegmentManager
         await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
     }
 
+    /// <inheritdoc />
+    public async Task DeleteSegmentsAsync(Guid itemId)
+    {
+        using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+        await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync().ConfigureAwait(false);
+    }
+
     /// <inheritdoc />
     public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
     {

+ 5 - 0
Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs

@@ -73,6 +73,11 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
             }
 
             offset += Limit;
+            if (offset > records)
+            {
+                offset = records;
+            }
+
             _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
         } while (offset < records);
 

+ 5 - 0
Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs

@@ -95,6 +95,11 @@ public class MoveExtractedFiles : IMigrationRoutine
             }
 
             offset += Limit;
+            if (offset > records)
+            {
+                offset = records;
+            }
+
             _logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
         } while (offset < records);
 

+ 131 - 0
Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs

@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to re-read creation dates for library items with internal metadata paths.
+/// </summary>
+[JellyfinMigration("2025-04-20T23:00:00", nameof(RefreshInternalDateModified), "32E762EB-4918-45CE-A44C-C801F66B877D", RunMigrationOnSetup = false)]
+public class RefreshInternalDateModified : IDatabaseMigrationRoutine
+{
+    private readonly ILogger<RefreshInternalDateModified> _logger;
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IFileSystem _fileSystem;
+    private readonly IServerApplicationHost _applicationHost;
+    private readonly bool _useFileCreationTimeForDateAdded;
+
+    private IReadOnlyList<string> _internalTypes = [
+         typeof(Genre).FullName!,
+         typeof(MusicGenre).FullName!,
+         typeof(MusicArtist).FullName!,
+         typeof(People).FullName!,
+         typeof(Studio).FullName!
+    ];
+
+    private IReadOnlyList<string> _internalPaths;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="RefreshInternalDateModified"/> class.
+    /// </summary>
+    /// <param name="applicationHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+    /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+    /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+    /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+    public RefreshInternalDateModified(
+        IServerApplicationHost applicationHost,
+        IServerApplicationPaths applicationPaths,
+        IServerConfigurationManager configurationManager,
+        IDbContextFactory<JellyfinDbContext> dbProvider,
+        ILogger<RefreshInternalDateModified> logger,
+        IFileSystem fileSystem)
+    {
+        _dbProvider = dbProvider;
+        _logger = logger;
+        _fileSystem = fileSystem;
+        _applicationHost = applicationHost;
+        _internalPaths = [
+            applicationPaths.ArtistsPath,
+            applicationPaths.GenrePath,
+            applicationPaths.MusicGenrePath,
+            applicationPaths.StudioPath,
+            applicationPaths.PeoplePath
+        ];
+        _useFileCreationTimeForDateAdded = configurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded;
+    }
+
+    /// <inheritdoc />
+    public void Perform()
+    {
+        const int Limit = 5000;
+        int itemCount = 0, offset = 0;
+
+        var sw = Stopwatch.StartNew();
+
+        using var context = _dbProvider.CreateDbContext();
+        var records = context.BaseItems.Count(b => _internalTypes.Contains(b.Type));
+        _logger.LogInformation("Checking if {Count} potentially internal items require refreshed DateModified", records);
+
+        do
+        {
+            var results = context.BaseItems
+                            .Where(b => _internalTypes.Contains(b.Type))
+                            .OrderBy(e => e.Id)
+                            .Skip(offset)
+                            .Take(Limit)
+                            .ToList();
+
+            foreach (var item in results)
+            {
+                var itemPath = item.Path;
+                if (itemPath is not null)
+                {
+                    var realPath = _applicationHost.ExpandVirtualPath(item.Path);
+                    if (_internalPaths.Any(path => realPath.StartsWith(path, StringComparison.Ordinal)))
+                    {
+                        var writeTime = _fileSystem.GetLastWriteTimeUtc(realPath);
+                        var itemModificationTime = item.DateModified;
+                        if (writeTime != itemModificationTime)
+                        {
+                            _logger.LogDebug("Reset file modification date: Old: {Old} - New: {New} - Path: {Path}", itemModificationTime, writeTime, realPath);
+                            item.DateModified = writeTime;
+                            if (_useFileCreationTimeForDateAdded)
+                            {
+                                item.DateCreated = _fileSystem.GetCreationTimeUtc(realPath);
+                            }
+
+                            itemCount++;
+                        }
+                    }
+                }
+            }
+
+            offset += Limit;
+            if (offset > records)
+            {
+                offset = records;
+            }
+
+            _logger.LogInformation("Checked: {Count} - Refreshed: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed);
+        } while (offset < records);
+
+        context.SaveChanges();
+
+        _logger.LogInformation("Refreshed DateModified for {Count} items in {Time}", itemCount, sw.Elapsed);
+    }
+}

+ 36 - 4
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -25,6 +25,7 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
@@ -1265,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities
         }
 
         /// <summary>
-        /// Overrides the base implementation to refresh metadata for local trailers.
+        /// The base implementation to refresh metadata.
         /// </summary>
         /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -1362,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities
 
         protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
         {
-            var path = ContainingFolderPath;
-
-            return directoryService.GetFileSystemEntries(path);
+            return directoryService.GetFileSystemEntries(ContainingFolderPath);
         }
 
         private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
@@ -1393,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities
                 return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
             });
 
+            // Cleanup removed extras
+            var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
+            if (removedExtraIds.Length > 0)
+            {
+                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
+                {
+                    ItemIds = removedExtraIds
+                });
+                foreach (var removedExtra in removedExtras)
+                {
+                    LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
+                    {
+                        DeleteFileLocation = false
+                    });
+                }
+            }
+
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
             item.ExtraIds = newExtraIds;
@@ -1407,6 +1423,22 @@ namespace MediaBrowser.Controller.Entities
 
         public virtual bool RequiresRefresh()
         {
+            if (string.IsNullOrEmpty(Path) || DateModified == default)
+            {
+                return false;
+            }
+
+            var info = FileSystem.GetFileSystemInfo(Path);
+            if (info.Exists)
+            {
+                if (info.IsDirectory)
+                {
+                    return info.LastWriteTimeUtc != DateModified;
+                }
+
+                return info.LastWriteTimeUtc != DateModified && info.Length != (Size ?? 0);
+            }
+
             return false;
         }
 

+ 9 - 1
MediaBrowser.Controller/IO/IPathManager.cs

@@ -1,9 +1,10 @@
+using System.Collections.Generic;
 using MediaBrowser.Controller.Entities;
 
 namespace MediaBrowser.Controller.IO;
 
 /// <summary>
-/// Interface ITrickplayManager.
+/// Interface IPathManager.
 /// </summary>
 public interface IPathManager
 {
@@ -60,4 +61,11 @@ public interface IPathManager
     /// <param name="chapterPositionTicks">The chapter position.</param>
     /// <returns>The chapter images data path.</returns>
     public string GetChapterImagePath(BaseItem item, long chapterPositionTicks);
+
+    /// <summary>
+    /// Gets the paths of extracted data folders.
+    /// </summary>
+    /// <param name="item">The base item.</param>
+    /// <returns>The absolute paths.</returns>
+    public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item);
 }

+ 37 - 0
MediaBrowser.Controller/Library/IKeyframeManager.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface IKeyframeManager.
+/// </summary>
+public interface IKeyframeManager
+{
+    /// <summary>
+    /// Gets the keyframe data.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <returns>The keyframe data.</returns>
+    IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId);
+
+    /// <summary>
+    /// Saves the keyframe data.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <param name="data">The keyframe data.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>The task object representing the asynchronous operation.</returns>
+    Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+
+    /// <summary>
+    /// Deletes the keyframe data.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>The task object representing the asynchronous operation.</returns>
+    Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken);
+}

+ 4 - 4
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -220,13 +220,13 @@ namespace MediaBrowser.Controller.Library
         /// <param name="resolvers">The resolvers.</param>
         /// <param name="introProviders">The intro providers.</param>
         /// <param name="itemComparers">The item comparers.</param>
-        /// <param name="postscanTasks">The postscan tasks.</param>
+        /// <param name="postScanTasks">The post scan tasks.</param>
         void AddParts(
             IEnumerable<IResolverIgnoreRule> rules,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IIntroProvider> introProviders,
             IEnumerable<IBaseItemComparer> itemComparers,
-            IEnumerable<ILibraryPostScanTask> postscanTasks);
+            IEnumerable<ILibraryPostScanTask> postScanTasks);
 
         /// <summary>
         /// Sorts the specified items.
@@ -593,11 +593,11 @@ namespace MediaBrowser.Controller.Library
         QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query);
 
         /// <summary>
-        /// Ignores the file.
+        /// Checks if the file is ignored.
         /// </summary>
         /// <param name="file">The file.</param>
         /// <param name="parent">The parent.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+        /// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns>
         bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
 
         Guid GetStudioId(string name);

+ 8 - 1
MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs

@@ -7,7 +7,7 @@ using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.MediaSegments;
 
-namespace MediaBrowser.Controller;
+namespace MediaBrowser.Controller.MediaSegments;
 
 /// <summary>
 /// Defines methods for interacting with media segments.
@@ -45,6 +45,13 @@ public interface IMediaSegmentManager
     /// <returns>a task.</returns>
     Task DeleteSegmentAsync(Guid segmentId);
 
+    /// <summary>
+    /// Deletes all media segments of an item.
+    /// </summary>
+    /// <param name="itemId">The <see cref="BaseItem.Id"/> to delete all segments for.</param>
+    /// <returns>a task.</returns>
+    Task DeleteSegmentsAsync(Guid itemId);
+
     /// <summary>
     /// Obtains all segments associated with the itemId.
     /// </summary>

+ 2 - 4
MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs

@@ -1,13 +1,11 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+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;
+namespace MediaBrowser.Controller.MediaSegments;
 
 /// <summary>
 /// Provides methods for Obtaining the Media Segments from an Item.

+ 8 - 0
MediaBrowser.Controller/Persistence/IKeyframeRepository.cs

@@ -26,4 +26,12 @@ public interface IKeyframeRepository
     /// <param name="cancellationToken">The cancellation token.</param>
     /// <returns>The task object representing the asynchronous operation.</returns>
     Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+
+    /// <summary>
+    /// Deletes the keyframe data.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>The task object representing the asynchronous operation.</returns>
+    Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken);
 }

+ 2 - 2
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -537,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 EnableRaisingEvents = true
             };
 
-            _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
+            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 
             var memoryStream = new MemoryStream();
             await using (memoryStream.ConfigureAwait(false))
@@ -637,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
+                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
                 }
             }
 

+ 48 - 32
MediaBrowser.Providers/Books/AudioBookMetadataService.cs

@@ -1,50 +1,66 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Books
+namespace MediaBrowser.Providers.Books;
+
+/// <summary>
+/// Service to manage audiobook metadata.
+/// </summary>
+public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo>
 {
-    public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="AudioBookMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public AudioBookMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<AudioBookMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public AudioBookMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<AudioBookMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(
-            MetadataResult<AudioBook> source,
-            MetadataResult<AudioBook> target,
-            MetadataField[] lockedFields,
-            bool replaceData,
-            bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(
+        MetadataResult<AudioBook> source,
+        MetadataResult<AudioBook> target,
+        MetadataField[] lockedFields,
+        bool replaceData,
+        bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || targetItem.Artists.Count == 0)
-            {
-                targetItem.Artists = sourceItem.Artists;
-            }
+        if (replaceData || targetItem.Artists.Count == 0)
+        {
+            targetItem.Artists = sourceItem.Artists;
+        }
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.Album))
-            {
-                targetItem.Album = sourceItem.Album;
-            }
+        if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+        {
+            targetItem.Album = sourceItem.Album;
         }
     }
 }

+ 37 - 21
MediaBrowser.Providers/Books/BookMetadataService.cs

@@ -1,37 +1,53 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Books
+namespace MediaBrowser.Providers.Books;
+
+/// <summary>
+/// Service to manage book metadata.
+/// </summary>
+public class BookMetadataService : MetadataService<Book, BookInfo>
 {
-    public class BookMetadataService : MetadataService<Book, BookInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BookMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public BookMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<BookMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public BookMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<BookMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName))
-            {
-                target.Item.SeriesName = source.Item.SeriesName;
-            }
+        if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName))
+        {
+            target.Item.SeriesName = source.Item.SeriesName;
         }
     }
 }

+ 70 - 54
MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs

@@ -1,85 +1,101 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.BoxSets
+namespace MediaBrowser.Providers.BoxSets;
+
+/// <summary>
+/// Service to manage boxset metadata.
+/// </summary>
+public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
 {
-    public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BoxSetMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public BoxSetMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<BoxSetMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public BoxSetMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<BoxSetMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingGenresFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingGenresFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingOfficialRatingFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingOfficialRatingFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingStudiosFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingStudiosFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingPremiereDateFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingPremiereDateFromChildren => true;
 
-        /// <inheritdoc />
-        protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
-        {
-            return item.GetLinkedChildren();
-        }
+    /// <inheritdoc />
+    protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
+    {
+        return item.GetLinkedChildren();
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (mergeMetadataSettings)
+        if (mergeMetadataSettings)
+        {
+            if (replaceData || targetItem.LinkedChildren.Length == 0)
+            {
+                targetItem.LinkedChildren = sourceItem.LinkedChildren;
+            }
+            else
             {
-                if (replaceData || targetItem.LinkedChildren.Length == 0)
-                {
-                    targetItem.LinkedChildren = sourceItem.LinkedChildren;
-                }
-                else
-                {
-                    targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
-                }
+                targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
             }
         }
+    }
 
-        /// <inheritdoc />
-        protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
-        {
-            var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
-            var libraryFolderIds = item.GetLibraryFolderIds();
+    /// <inheritdoc />
+    protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
+    {
+        var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
 
-            var itemLibraryFolderIds = item.LibraryFolderIds;
-            if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
-            {
-                item.LibraryFolderIds = libraryFolderIds;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
+        var libraryFolderIds = item.GetLibraryFolderIds();
 
-            return updatedType;
+        var itemLibraryFolderIds = item.LibraryFolderIds;
+        if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
+        {
+            item.LibraryFolderIds = libraryFolderIds;
+            updatedType |= ItemUpdateType.MetadataImport;
         }
+
+        return updatedType;
     }
 }

+ 29 - 13
MediaBrowser.Providers/Channels/ChannelMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Channels
+namespace MediaBrowser.Providers.Channels;
+
+/// <summary>
+/// Service to manage channel metadata.
+/// </summary>
+public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
 {
-    public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ChannelMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public ChannelMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<ChannelMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public ChannelMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<ChannelMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage collection folder metadata.
+/// </summary>
+public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo>
 {
-    public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CollectionFolderMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public CollectionFolderMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<CollectionFolderMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public CollectionFolderMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<CollectionFolderMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 33 - 17
MediaBrowser.Providers/Folders/FolderMetadataService.cs

@@ -1,29 +1,45 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage folder metadata.
+/// </summary>
+public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
 {
-    public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="FolderMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public FolderMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<FolderMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public FolderMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<FolderMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
-
-        /// <inheritdoc />
-        // Make sure the type-specific services get picked first
-        public override int Order => 10;
     }
+
+    /// <inheritdoc />
+    // Make sure the type-specific services get picked first
+    public override int Order => 10;
 }

+ 29 - 13
MediaBrowser.Providers/Folders/UserViewMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.Providers.Folders;
+
+/// <summary>
+/// Service to manage user view metadata.
+/// </summary>
+public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
 {
-    public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="UserViewMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public UserViewMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<UserViewMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public UserViewMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<UserViewMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/Genres/GenreMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Genres
+namespace MediaBrowser.Providers.Genres;
+
+/// <summary>
+/// Service to manage genre metadata.
+/// </summary>
+public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
 {
-    public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="GenreMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public GenreMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<GenreMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public GenreMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<GenreMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.LiveTv
+namespace MediaBrowser.Providers.LiveTv;
+
+/// <summary>
+/// Service to manage live TV metadata.
+/// </summary>
+public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo>
 {
-    public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="LiveTvMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public LiveTvMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<LiveTvMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public LiveTvMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<LiveTvMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 75 - 1
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -4,6 +4,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Net.Http;
 using System.Threading;
@@ -12,7 +13,9 @@ using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
@@ -26,13 +29,24 @@ namespace MediaBrowser.Providers.Manager
         where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
         where TIdType : ItemLookupInfo, new()
     {
-        protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
+        protected MetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger<MetadataService<TItemType, TIdType>> logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager,
+            IPathManager pathManager,
+            IKeyframeManager keyframeManager,
+            IMediaSegmentManager mediaSegmentManager)
         {
             ServerConfigurationManager = serverConfigurationManager;
             Logger = logger;
             ProviderManager = providerManager;
             FileSystem = fileSystem;
             LibraryManager = libraryManager;
+            PathManager = pathManager;
+            KeyframeManager = keyframeManager;
+            MediaSegmentManager = mediaSegmentManager;
             ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
         }
 
@@ -48,6 +62,12 @@ namespace MediaBrowser.Providers.Manager
 
         protected ILibraryManager LibraryManager { get; }
 
+        protected IPathManager PathManager { get; }
+
+        protected IKeyframeManager KeyframeManager { get; }
+
+        protected IMediaSegmentManager MediaSegmentManager { get; }
+
         protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
 
         protected virtual bool EnableUpdatingGenresFromChildren => false;
@@ -303,6 +323,55 @@ namespace MediaBrowser.Providers.Manager
                 updateType |= ItemUpdateType.MetadataImport;
             }
 
+            // Cleanup extracted files if source file was modified
+            var itemPath = item.Path;
+            if (!string.IsNullOrEmpty(itemPath))
+            {
+                var info = FileSystem.GetFileSystemInfo(itemPath);
+                var modificationDate = info.LastWriteTimeUtc;
+                var itemLastModifiedFileSystem = item.DateModified;
+                if (info.Exists && itemLastModifiedFileSystem != modificationDate)
+                {
+                    Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath);
+
+                    item.DateModified = modificationDate;
+                    if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
+                    {
+                        item.DateCreated = info.CreationTimeUtc;
+                    }
+
+                    var size = info.Length;
+                    if (item is Video video)
+                    {
+                        var videoType = video.VideoType;
+                        var sizeChanged = size != (video.Size ?? 0);
+                        if (videoType == VideoType.BluRay || video.VideoType == VideoType.Dvd || sizeChanged)
+                        {
+                            if (sizeChanged)
+                            {
+                                item.Size = size;
+                                Logger.LogDebug("File size changed from {Then} to {Now}: {Path}", video.Size, size, itemPath);
+                            }
+
+                            var validPaths = PathManager.GetExtractedDataPaths(video).Where(Directory.Exists).ToList();
+                            if (validPaths.Count > 0)
+                            {
+                                Logger.LogInformation("File changed, pruning extracted data: {Path}", itemPath);
+                                foreach (var path in validPaths)
+                                {
+                                    Directory.Delete(path, true);
+                                }
+                            }
+
+                            KeyframeManager.DeleteKeyframeDataAsync(video.Id, CancellationToken.None).GetAwaiter().GetResult();
+                            MediaSegmentManager.DeleteSegmentsAsync(item.Id).GetAwaiter().GetResult();
+                        }
+                    }
+
+                    updateType |= ItemUpdateType.MetadataImport;
+                }
+            }
+
             return updateType;
         }
 
@@ -1132,6 +1201,11 @@ namespace MediaBrowser.Providers.Manager
                     target.DateCreated = source.DateCreated;
                 }
 
+                if (replaceData || source.DateModified != default)
+                {
+                    target.DateModified = source.DateModified;
+                }
+
                 if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
                 {
                     target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;

+ 1 - 2
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -1,13 +1,11 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 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;
@@ -24,6 +22,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Configuration;

+ 0 - 1
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -133,7 +133,6 @@ namespace MediaBrowser.Providers.MediaInfo
             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<MediaStream>(mediaInfo.MediaStreams);

+ 10 - 6
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -214,10 +214,14 @@ namespace MediaBrowser.Providers.MediaInfo
                 mediaAttachments = mediaInfo.MediaAttachments;
                 video.TotalBitrate = mediaInfo.Bitrate;
                 video.RunTimeTicks = mediaInfo.RunTimeTicks;
-                video.Size = mediaInfo.Size;
                 video.Container = mediaInfo.Container;
+                var videoType = video.VideoType;
+                if (videoType == VideoType.BluRay || videoType == VideoType.Dvd)
+                {
+                    video.Size = mediaInfo.Size;
+                }
 
-                chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
+                chapters = mediaInfo.Chapters ?? [];
                 if (blurayInfo is not null)
                 {
                     FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
@@ -234,8 +238,8 @@ namespace MediaBrowser.Providers.MediaInfo
                     }
                 }
 
-                mediaAttachments = Array.Empty<MediaAttachment>();
-                chapters = Array.Empty<ChapterInfo>();
+                mediaAttachments = [];
+                chapters = [];
             }
 
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
@@ -400,7 +404,7 @@ namespace MediaBrowser.Providers.MediaInfo
             {
                 if (video.Genres.Length == 0 || replaceData)
                 {
-                    video.Genres = Array.Empty<string>();
+                    video.Genres = [];
 
                     foreach (var genre in data.Genres.Trimmed())
                     {
@@ -643,7 +647,7 @@ namespace MediaBrowser.Providers.MediaInfo
             long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
             if (runtime <= dummyChapterDuration)
             {
-                return Array.Empty<ChapterInfo>();
+                return [];
             }
 
             int chapterCount = (int)(runtime / dummyChapterDuration);

+ 2 - 2
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -130,9 +130,9 @@ namespace MediaBrowser.Providers.MediaInfo
                 if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
                 {
                     var file = directoryService.GetFile(path);
-                    if (file is not null && file.LastWriteTimeUtc != item.DateModified)
+                    if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size)
                     {
-                        _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
+                        _logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
                         return true;
                     }
                 }

+ 0 - 1
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Tasks;

+ 39 - 23
MediaBrowser.Providers/Movies/MovieMetadataService.cs

@@ -1,40 +1,56 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.Providers.Movies;
+
+/// <summary>
+/// Service to manage movie metadata.
+/// </summary>
+public class MovieMetadataService : MetadataService<Movie, MovieInfo>
 {
-    public class MovieMetadataService : MetadataService<Movie, MovieInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MovieMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public MovieMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<MovieMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public MovieMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<MovieMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName))
-            {
-                targetItem.CollectionName = sourceItem.CollectionName;
-            }
+        if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName))
+        {
+            targetItem.CollectionName = sourceItem.CollectionName;
         }
     }
 }

+ 40 - 24
MediaBrowser.Providers/Movies/TrailerMetadataService.cs

@@ -1,42 +1,58 @@
-#pragma warning disable CS1591
-
 using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.Providers.Movies;
+
+/// <summary>
+/// Service to manage trailer metadata.
+/// </summary>
+public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
 {
-    public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TrailerMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public TrailerMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<TrailerMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
+    {
+    }
+
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
     {
-        public TrailerMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<TrailerMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+        if (replaceData || target.Item.TrailerTypes.Length == 0)
         {
+            target.Item.TrailerTypes = source.Item.TrailerTypes;
         }
-
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        else
         {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
-
-            if (replaceData || target.Item.TrailerTypes.Length == 0)
-            {
-                target.Item.TrailerTypes = source.Item.TrailerTypes;
-            }
-            else
-            {
-                target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
-            }
+            target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
         }
     }
 }

+ 183 - 176
MediaBrowser.Providers/Music/AlbumMetadataService.cs

@@ -5,245 +5,252 @@ using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// The album metadata service.
+/// </summary>
+public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
 {
     /// <summary>
-    /// The album metadata service.
+    /// Initializes a new instance of the <see cref="AlbumMetadataService"/> class.
     /// </summary>
-    public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public AlbumMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<AlbumMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AlbumMetadataService"/> class.
-        /// </summary>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
-        /// <param name="logger">Instance of the <see cref="ILogger"/> 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 AlbumMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<AlbumMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
+
+    /// <inheritdoc />
+    protected override bool EnableUpdatingPremiereDateFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingPremiereDateFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingGenresFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingGenresFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingStudiosFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingStudiosFromChildren => true;
+    /// <inheritdoc />
+    protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
+        => item.GetRecursiveChildren(i => i is Audio);
 
-        /// <inheritdoc />
-        protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
-            => item.GetRecursiveChildren(i => i is Audio);
+    /// <inheritdoc />
+    protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+    {
+        var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
 
-        /// <inheritdoc />
-        protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+        // don't update user-changeable metadata for locked items
+        if (item.IsLocked)
         {
-            var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+            return updateType;
+        }
 
-            // don't update user-changeable metadata for locked items
-            if (item.IsLocked)
+        if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
+        {
+            if (!item.LockedFields.Contains(MetadataField.Name))
             {
-                return updateType;
-            }
+                var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i));
 
-            if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
-            {
-                if (!item.LockedFields.Contains(MetadataField.Name))
+                if (!string.IsNullOrEmpty(name)
+                    && !string.Equals(item.Name, name, StringComparison.Ordinal))
                 {
-                    var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i));
-
-                    if (!string.IsNullOrEmpty(name)
-                        && !string.Equals(item.Name, name, StringComparison.Ordinal))
-                    {
-                        item.Name = name;
-                        updateType |= ItemUpdateType.MetadataEdit;
-                    }
+                    item.Name = name;
+                    updateType |= ItemUpdateType.MetadataEdit;
                 }
-
-                var songs = children.Cast<Audio>().ToArray();
-
-                updateType |= SetArtistsFromSongs(item, songs);
-                updateType |= SetAlbumArtistFromSongs(item, songs);
-                updateType |= SetAlbumFromSongs(item, songs);
-                updateType |= SetPeople(item);
             }
 
-            return updateType;
+            var songs = children.Cast<Audio>().ToArray();
+
+            updateType |= SetArtistsFromSongs(item, songs);
+            updateType |= SetAlbumArtistFromSongs(item, songs);
+            updateType |= SetAlbumFromSongs(item, songs);
+            updateType |= SetPeople(item);
         }
 
-        private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
-        {
-            var updateType = ItemUpdateType.None;
+        return updateType;
+    }
 
-            var albumArtists = songs
-                .SelectMany(i => i.AlbumArtists)
-                .GroupBy(i => i)
-                .OrderByDescending(g => g.Count())
-                .Select(g => g.Key)
-                .ToArray();
+    private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+    {
+        var updateType = ItemUpdateType.None;
 
-            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
+        var albumArtists = songs
+            .SelectMany(i => i.AlbumArtists)
+            .GroupBy(i => i)
+            .OrderByDescending(g => g.Count())
+            .Select(g => g.Key)
+            .ToArray();
 
-            if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
-            {
-                item.AlbumArtists = albumArtists;
-                updateType |= ItemUpdateType.MetadataEdit;
-            }
+        updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
 
-            return updateType;
+        if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
+        {
+            item.AlbumArtists = albumArtists;
+            updateType |= ItemUpdateType.MetadataEdit;
         }
 
-        private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
-        {
-            var updateType = ItemUpdateType.None;
+        return updateType;
+    }
 
-            var artists = songs
-                .SelectMany(i => i.Artists)
-                .GroupBy(i => i)
-                .OrderByDescending(g => g.Count())
-                .Select(g => g.Key)
-                .ToArray();
+    private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+    {
+        var updateType = ItemUpdateType.None;
 
-            if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
-            {
-                item.Artists = artists;
-                updateType |= ItemUpdateType.MetadataEdit;
-            }
+        var artists = songs
+            .SelectMany(i => i.Artists)
+            .GroupBy(i => i)
+            .OrderByDescending(g => g.Count())
+            .Select(g => g.Key)
+            .ToArray();
 
-            return updateType;
+        if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
+        {
+            item.Artists = artists;
+            updateType |= ItemUpdateType.MetadataEdit;
         }
 
-        private ItemUpdateType SetAlbumFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
-        {
-            var updateType = ItemUpdateType.None;
+        return updateType;
+    }
 
-            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbum);
-            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzReleaseGroup);
+    private ItemUpdateType SetAlbumFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+    {
+        var updateType = ItemUpdateType.None;
 
-            return updateType;
-        }
+        updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbum);
+        updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzReleaseGroup);
 
-        private ItemUpdateType SetProviderIdFromSongs(BaseItem item, IReadOnlyList<Audio> songs, MetadataProvider provider)
+        return updateType;
+    }
+
+    private ItemUpdateType SetProviderIdFromSongs(BaseItem item, IReadOnlyList<Audio> songs, MetadataProvider provider)
+    {
+        var ids = songs
+            .Select(i => i.GetProviderId(provider))
+            .GroupBy(i => i)
+            .OrderByDescending(g => g.Count())
+            .Select(g => g.Key)
+            .ToArray();
+
+        var id = item.GetProviderId(provider);
+        if (ids.Length != 0)
         {
-            var ids = songs
-                .Select(i => i.GetProviderId(provider))
-                .GroupBy(i => i)
-                .OrderByDescending(g => g.Count())
-                .Select(g => g.Key)
-                .ToArray();
-
-            var id = item.GetProviderId(provider);
-            if (ids.Length != 0)
+            var firstId = ids[0];
+            if (!string.IsNullOrEmpty(firstId)
+                && (string.IsNullOrEmpty(id)
+                    || !id.Equals(firstId, StringComparison.OrdinalIgnoreCase)))
             {
-                var firstId = ids[0];
-                if (!string.IsNullOrEmpty(firstId)
-                    && (string.IsNullOrEmpty(id)
-                        || !id.Equals(firstId, StringComparison.OrdinalIgnoreCase)))
-                {
-                    item.SetProviderId(provider, firstId);
-                    return ItemUpdateType.MetadataEdit;
-                }
+                item.SetProviderId(provider, firstId);
+                return ItemUpdateType.MetadataEdit;
             }
-
-            return ItemUpdateType.None;
         }
 
-        private void SetProviderId(MusicAlbum sourceItem, MusicAlbum targetItem, MetadataProvider provider)
+        return ItemUpdateType.None;
+    }
+
+    private void SetProviderId(MusicAlbum sourceItem, MusicAlbum targetItem, MetadataProvider provider)
+    {
+        var source = sourceItem.GetProviderId(provider);
+        var target = targetItem.GetProviderId(provider);
+        if (!string.IsNullOrEmpty(source)
+            && (string.IsNullOrEmpty(target)
+                || !target.Equals(source, StringComparison.Ordinal)))
         {
-            var source = sourceItem.GetProviderId(provider);
-            var target = targetItem.GetProviderId(provider);
-            if (!string.IsNullOrEmpty(source)
-                && (string.IsNullOrEmpty(target)
-                    || !target.Equals(source, StringComparison.Ordinal)))
-            {
-                targetItem.SetProviderId(provider, source);
-            }
+            targetItem.SetProviderId(provider, source);
         }
+    }
 
-        private ItemUpdateType SetPeople(MusicAlbum item)
+    private ItemUpdateType SetPeople(MusicAlbum item)
+    {
+        var updateType = ItemUpdateType.None;
+
+        if (item.AlbumArtists.Any() || item.Artists.Any())
         {
-            var updateType = ItemUpdateType.None;
+            var people = new List<PersonInfo>();
 
-            if (item.AlbumArtists.Any() || item.Artists.Any())
+            foreach (var albumArtist in item.AlbumArtists)
             {
-                var people = new List<PersonInfo>();
-
-                foreach (var albumArtist in item.AlbumArtists)
+                PeopleHelper.AddPerson(people, new PersonInfo
                 {
-                    PeopleHelper.AddPerson(people, new PersonInfo
-                    {
-                        Name = albumArtist.Trim(),
-                        Type = PersonKind.AlbumArtist
-                    });
-                }
+                    Name = albumArtist.Trim(),
+                    Type = PersonKind.AlbumArtist
+                });
+            }
 
-                foreach (var artist in item.Artists)
+            foreach (var artist in item.Artists)
+            {
+                PeopleHelper.AddPerson(people, new PersonInfo
                 {
-                    PeopleHelper.AddPerson(people, new PersonInfo
-                    {
-                        Name = artist.Trim(),
-                        Type = PersonKind.Artist
-                    });
-                }
-
-                LibraryManager.UpdatePeople(item, people);
-                updateType |= ItemUpdateType.MetadataEdit;
+                    Name = artist.Trim(),
+                    Type = PersonKind.Artist
+                });
             }
 
-            return updateType;
+            LibraryManager.UpdatePeople(item, people);
+            updateType |= ItemUpdateType.MetadataEdit;
         }
 
-        /// <inheritdoc />
-        protected override void MergeData(
-            MetadataResult<MusicAlbum> source,
-            MetadataResult<MusicAlbum> target,
-            MetadataField[] lockedFields,
-            bool replaceData,
-            bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        return updateType;
+    }
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+    /// <inheritdoc />
+    protected override void MergeData(
+        MetadataResult<MusicAlbum> source,
+        MetadataResult<MusicAlbum> target,
+        MetadataField[] lockedFields,
+        bool replaceData,
+        bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            if (replaceData || targetItem.Artists.Count == 0)
-            {
-                targetItem.Artists = sourceItem.Artists;
-            }
-            else
-            {
-                targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
-            }
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
-            {
-                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbumArtist);
-            }
+        if (replaceData || targetItem.Artists.Count == 0)
+        {
+            targetItem.Artists = sourceItem.Artists;
+        }
+        else
+        {
+            targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+        }
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbum)))
-            {
-                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbum);
-            }
+        if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
+        {
+            SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbumArtist);
+        }
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)))
-            {
-                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzReleaseGroup);
-            }
+        if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbum)))
+        {
+            SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbum);
+        }
+
+        if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)))
+        {
+            SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzReleaseGroup);
         }
     }
 }

+ 42 - 27
MediaBrowser.Providers/Music/ArtistMetadataService.cs

@@ -1,43 +1,58 @@
-#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;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// Service to manage artist metadata.
+/// </summary>
+public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
 {
-    public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ArtistMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public ArtistMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<ArtistMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public ArtistMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<ArtistMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingGenresFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingGenresFromChildren => true;
 
-        /// <inheritdoc />
-        protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
-        {
-            return item.IsAccessedByName
-                ? item.GetTaggedItems(new InternalItemsQuery
-                {
-                    Recursive = true,
-                    IsFolder = false
-                })
-                : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
-        }
+    /// <inheritdoc />
+    protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
+    {
+        return item.IsAccessedByName
+            ? item.GetTaggedItems(new InternalItemsQuery
+            {
+                Recursive = true,
+                IsFolder = false
+            })
+            : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
     }
 }

+ 59 - 52
MediaBrowser.Providers/Music/AudioMetadataService.cs

@@ -2,78 +2,85 @@ using System;
 using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// The audio metadata service.
+/// </summary>
+public class AudioMetadataService : MetadataService<Audio, SongInfo>
 {
     /// <summary>
-    /// The audio metadata service.
+    /// Initializes a new instance of the <see cref="AudioMetadataService"/> class.
     /// </summary>
-    public class AudioMetadataService : MetadataService<Audio, SongInfo>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public AudioMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<AudioMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AudioMetadataService"/> class.
-        /// </summary>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
-        /// <param name="logger">Instance of the <see cref="ILogger"/> 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 AudioMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<AudioMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        private void SetProviderId(Audio sourceItem, Audio targetItem, bool replaceData, MetadataProvider provider)
+    private void SetProviderId(Audio sourceItem, Audio targetItem, bool replaceData, MetadataProvider provider)
+    {
+        var target = targetItem.GetProviderId(provider);
+        if (replaceData || string.IsNullOrEmpty(target))
         {
-            var target = targetItem.GetProviderId(provider);
-            if (replaceData || string.IsNullOrEmpty(target))
+            var source = sourceItem.GetProviderId(provider);
+            if (!string.IsNullOrEmpty(source)
+                && (string.IsNullOrEmpty(target)
+                    || !target.Equals(source, StringComparison.Ordinal)))
             {
-                var source = sourceItem.GetProviderId(provider);
-                if (!string.IsNullOrEmpty(source)
-                    && (string.IsNullOrEmpty(target)
-                        || !target.Equals(source, StringComparison.Ordinal)))
-                {
-                    targetItem.SetProviderId(provider, source);
-                }
+                targetItem.SetProviderId(provider, source);
             }
         }
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || targetItem.Artists.Count == 0)
-            {
-                targetItem.Artists = sourceItem.Artists;
-            }
-            else
-            {
-                targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
-            }
-
-            if (replaceData || string.IsNullOrEmpty(targetItem.Album))
-            {
-                targetItem.Album = sourceItem.Album;
-            }
+        if (replaceData || targetItem.Artists.Count == 0)
+        {
+            targetItem.Artists = sourceItem.Artists;
+        }
+        else
+        {
+            targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+        }
 
-            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
-            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
-            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
+        if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+        {
+            targetItem.Album = sourceItem.Album;
         }
+
+        SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
+        SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
+        SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
     }
 }

+ 52 - 36
MediaBrowser.Providers/Music/MusicVideoMetadataService.cs

@@ -1,56 +1,72 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.Providers.Music;
+
+/// <summary>
+/// Service to manage music video metadata.
+/// </summary>
+public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
 {
-    public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MusicVideoMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public MusicVideoMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<MusicVideoMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public MusicVideoMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<MusicVideoMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override void MergeData(
-            MetadataResult<MusicVideo> source,
-            MetadataResult<MusicVideo> target,
-            MetadataField[] lockedFields,
-            bool replaceData,
-            bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(
+        MetadataResult<MusicVideo> source,
+        MetadataResult<MusicVideo> target,
+        MetadataField[] lockedFields,
+        bool replaceData,
+        bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.Album))
-            {
-                targetItem.Album = sourceItem.Album;
-            }
+        if (replaceData || string.IsNullOrEmpty(targetItem.Album))
+        {
+            targetItem.Album = sourceItem.Album;
+        }
 
-            if (replaceData || targetItem.Artists.Count == 0)
-            {
-                targetItem.Artists = sourceItem.Artists;
-            }
-            else
-            {
-                targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
-            }
+        if (replaceData || targetItem.Artists.Count == 0)
+        {
+            targetItem.Artists = sourceItem.Artists;
+        }
+        else
+        {
+            targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
         }
     }
 }

+ 29 - 13
MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.MusicGenres
+namespace MediaBrowser.Providers.MusicGenres;
+
+/// <summary>
+/// Service to manage music genre metadata.
+/// </summary>
+public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupInfo>
 {
-    public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MusicGenreMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public MusicGenreMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<MusicGenreMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public MusicGenreMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<MusicGenreMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/People/PersonMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.People
+namespace MediaBrowser.Providers.People;
+
+/// <summary>
+/// Service to manage person metadata.
+/// </summary>
+public class PersonMetadataService : MetadataService<Person, PersonLookupInfo>
 {
-    public class PersonMetadataService : MetadataService<Person, PersonLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PersonMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public PersonMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<PersonMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public PersonMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<PersonMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Photos
+namespace MediaBrowser.Providers.Photos;
+
+/// <summary>
+/// Service to manage photo album metadata.
+/// </summary>
+public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
 {
-    public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PhotoAlbumMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public PhotoAlbumMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<PhotoAlbumMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public PhotoAlbumMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<PhotoAlbumMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 29 - 13
MediaBrowser.Providers/Photos/PhotoMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Photos
+namespace MediaBrowser.Providers.Photos;
+
+/// <summary>
+/// Service to manage photo metadata.
+/// </summary>
+public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
 {
-    public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PhotoMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public PhotoMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<PhotoMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public PhotoMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<PhotoMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 63 - 47
MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs

@@ -1,10 +1,10 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -12,62 +12,78 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Playlists
+namespace MediaBrowser.Providers.Playlists;
+
+/// <summary>
+/// Service to manage playlist metadata.
+/// </summary>
+public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
 {
-    public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PlaylistMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public PlaylistMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<PlaylistMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public PlaylistMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<PlaylistMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingGenresFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingGenresFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingOfficialRatingFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingOfficialRatingFromChildren => true;
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingStudiosFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingStudiosFromChildren => true;
 
-        /// <inheritdoc />
-        protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
-            => item.GetLinkedChildren();
+    /// <inheritdoc />
+    protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
+        => item.GetLinkedChildren();
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (mergeMetadataSettings)
-            {
-                targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
+        if (mergeMetadataSettings)
+        {
+            targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
 
-                if (replaceData || targetItem.LinkedChildren.Length == 0)
-                {
-                    targetItem.LinkedChildren = sourceItem.LinkedChildren;
-                }
-                else
-                {
-                    targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
-                }
+            if (replaceData || targetItem.LinkedChildren.Length == 0)
+            {
+                targetItem.LinkedChildren = sourceItem.LinkedChildren;
+            }
+            else
+            {
+                targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
+            }
 
-                if (replaceData || targetItem.Shares.Count == 0)
-                {
-                    targetItem.Shares = sourceItem.Shares;
-                }
-                else
-                {
-                    targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
-                }
+            if (replaceData || targetItem.Shares.Count == 0)
+            {
+                targetItem.Shares = sourceItem.Shares;
+            }
+            else
+            {
+                targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
             }
         }
     }

+ 29 - 13
MediaBrowser.Providers/Studios/StudioMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Studios
+namespace MediaBrowser.Providers.Studios;
+
+/// <summary>
+/// Service to manage studio metadata.
+/// </summary>
+public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo>
 {
-    public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="StudioMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public StudioMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<StudioMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public StudioMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<StudioMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 83 - 81
MediaBrowser.Providers/TV/EpisodeMetadataService.cs

@@ -1,113 +1,115 @@
 using System;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage episode metadata.
+/// </summary>
+public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
 {
     /// <summary>
-    /// Service to manage episode metadata.
+    /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class.
     /// </summary>
-    public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public EpisodeMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<EpisodeMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
+    {
+    }
+
+    /// <inheritdoc />
+    protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
     {
-        /// <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,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
+
+        var seriesName = item.FindSeriesName();
+        if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
         {
+            item.SeriesName = seriesName;
+            updatedType |= ItemUpdateType.MetadataImport;
         }
 
-        /// <inheritdoc />
-        protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
+        var seasonName = item.FindSeasonName();
+        if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
         {
-            var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
-            var seriesName = item.FindSeriesName();
-            if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
-            {
-                item.SeriesName = seriesName;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
-
-            var seasonName = item.FindSeasonName();
-            if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
-            {
-                item.SeasonName = seasonName;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
-
-            var seriesId = item.FindSeriesId();
-            if (!item.SeriesId.Equals(seriesId))
-            {
-                item.SeriesId = seriesId;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
-
-            var seasonId = item.FindSeasonId();
-            if (!item.SeasonId.Equals(seasonId))
-            {
-                item.SeasonId = seasonId;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
+            item.SeasonName = seasonName;
+            updatedType |= ItemUpdateType.MetadataImport;
+        }
 
-            var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
-            if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
-            {
-                item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
+        var seriesId = item.FindSeriesId();
+        if (!item.SeriesId.Equals(seriesId))
+        {
+            item.SeriesId = seriesId;
+            updatedType |= ItemUpdateType.MetadataImport;
+        }
 
-            return updatedType;
+        var seasonId = item.FindSeasonId();
+        if (!item.SeasonId.Equals(seasonId))
+        {
+            item.SeasonId = seasonId;
+            updatedType |= ItemUpdateType.MetadataImport;
         }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
+        if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
         {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+            item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+            updatedType |= ItemUpdateType.MetadataImport;
+        }
+
+        return updatedType;
+    }
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 
-            if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue)
-            {
-                targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber;
-            }
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
 
-            if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue)
-            {
-                targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber;
-            }
+        if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue)
+        {
+            targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber;
+        }
 
-            if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue)
-            {
-                targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber;
-            }
+        if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue)
+        {
+            targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber;
+        }
 
-            if (replaceData || !targetItem.IndexNumberEnd.HasValue)
-            {
-                targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
-            }
+        if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue)
+        {
+            targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber;
+        }
 
-            if (replaceData || !targetItem.ParentIndexNumber.HasValue)
-            {
-                targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber;
-            }
+        if (replaceData || !targetItem.IndexNumberEnd.HasValue)
+        {
+            targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
         }
     }
 }

+ 81 - 74
MediaBrowser.Providers/TV/SeasonMetadataService.cs

@@ -4,109 +4,116 @@ using System.Linq;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage season metadata.
+/// </summary>
+public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
 {
     /// <summary>
-    /// Service to manage season metadata.
+    /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class.
     /// </summary>
-    public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public SeasonMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<SeasonMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        /// <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,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
+    }
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingPremiereDateFromChildren => true;
+    /// <inheritdoc />
+    protected override bool EnableUpdatingPremiereDateFromChildren => true;
 
-        /// <inheritdoc />
-        protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
-        {
-            var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
-
-            if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name))
-            {
-                var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName;
+    /// <inheritdoc />
+    protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
+    {
+        var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
 
-                if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
-                {
-                    item.Name = seasonZeroDisplayName;
-                    updatedType |= ItemUpdateType.MetadataEdit;
-                }
-            }
+        if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name))
+        {
+            var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName;
 
-            var seriesName = item.FindSeriesName();
-            if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
+            if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
             {
-                item.SeriesName = seriesName;
-                updatedType |= ItemUpdateType.MetadataImport;
+                item.Name = seasonZeroDisplayName;
+                updatedType |= ItemUpdateType.MetadataEdit;
             }
+        }
 
-            var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
-            if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
-            {
-                item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
+        var seriesName = item.FindSeriesName();
+        if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
+        {
+            item.SeriesName = seriesName;
+            updatedType |= ItemUpdateType.MetadataImport;
+        }
 
-            var seriesId = item.FindSeriesId();
-            if (!item.SeriesId.Equals(seriesId))
-            {
-                item.SeriesId = seriesId;
-                updatedType |= ItemUpdateType.MetadataImport;
-            }
+        var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
+        if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
+        {
+            item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+            updatedType |= ItemUpdateType.MetadataImport;
+        }
 
-            return updatedType;
+        var seriesId = item.FindSeriesId();
+        if (!item.SeriesId.Equals(seriesId))
+        {
+            item.SeriesId = seriesId;
+            updatedType |= ItemUpdateType.MetadataImport;
         }
 
-        /// <inheritdoc />
-        protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
-            => item.GetEpisodes();
+        return updatedType;
+    }
 
-        /// <inheritdoc />
-        protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
-        {
-            var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+    /// <inheritdoc />
+    protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
+        => item.GetEpisodes();
 
-            if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
-            {
-                updateType |= SaveIsVirtualItem(item, children);
-            }
+    /// <inheritdoc />
+    protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+    {
+        var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
 
-            return updateType;
+        if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
+        {
+            updateType |= SaveIsVirtualItem(item, children);
         }
 
-        private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
-        {
-            var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
+        return updateType;
+    }
 
-            if (item.IsVirtualItem != isVirtualItem)
-            {
-                item.IsVirtualItem = isVirtualItem;
-                return ItemUpdateType.MetadataEdit;
-            }
+    private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
+    {
+        var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
 
-            return ItemUpdateType.None;
+        if (item.IsVirtualItem != isVirtualItem)
+        {
+            item.IsVirtualItem = isVirtualItem;
+            return ItemUpdateType.MetadataEdit;
         }
+
+        return ItemUpdateType.None;
     }
 }

+ 213 - 206
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -8,7 +8,9 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
@@ -16,269 +18,274 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// Service to manage series metadata.
+/// </summary>
+public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
 {
+    private readonly ILocalizationManager _localizationManager;
+
     /// <summary>
-    /// Service to manage series metadata.
+    /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class.
     /// </summary>
-    public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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>
+    /// <param name="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public SeriesMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<SeriesMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        ILocalizationManager localizationManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        private readonly ILocalizationManager _localizationManager;
+        _localizationManager = 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,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager,
-            ILocalizationManager localizationManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+    /// <inheritdoc />
+    public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+    {
+        if (item is Series series)
         {
-            _localizationManager = localizationManager;
-        }
+            var seasons = series.GetRecursiveChildren(i => i is Season).ToList();
 
-        /// <inheritdoc />
-        public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
-        {
-            if (item is Series series)
+            foreach (var season in seasons)
             {
-                var seasons = series.GetRecursiveChildren(i => i is Season).ToList();
-
-                foreach (var season in seasons)
+                var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
+                if (hasUpdate)
                 {
-                    var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
-                    if (hasUpdate)
-                    {
-                        await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
-                    }
+                    await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
                 }
             }
-
-            return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false);
         }
 
-        /// <inheritdoc />
-        protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
-        {
-            await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+        return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+    }
 
-            RemoveObsoleteEpisodes(item);
-            RemoveObsoleteSeasons(item);
-            await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
+    /// <inheritdoc />
+    protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+    {
+        await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+
+        RemoveObsoleteEpisodes(item);
+        RemoveObsoleteSeasons(item);
+        await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
+    }
+
+    /// <inheritdoc />
+    protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+    {
+        base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+        var sourceItem = source.Item;
+        var targetItem = target.Item;
+
+        if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
+        {
+            targetItem.AirTime = sourceItem.AirTime;
         }
 
-        /// <inheritdoc />
-        protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        if (replaceData || !targetItem.Status.HasValue)
         {
-            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+            targetItem.Status = sourceItem.Status;
+        }
 
-            var sourceItem = source.Item;
-            var targetItem = target.Item;
+        if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0)
+        {
+            targetItem.AirDays = sourceItem.AirDays;
+        }
+    }
 
-            if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
+    private void RemoveObsoleteSeasons(Series series)
+    {
+        // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
+        var physicalSeasonNumbers = new HashSet<int>();
+        var virtualSeasons = new List<Season>();
+        foreach (var existingSeason in series.Children.OfType<Season>())
+        {
+            if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
             {
-                targetItem.AirTime = sourceItem.AirTime;
+                physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
             }
-
-            if (replaceData || !targetItem.Status.HasValue)
+            else if (existingSeason.LocationType == LocationType.Virtual)
             {
-                targetItem.Status = sourceItem.Status;
+                virtualSeasons.Add(existingSeason);
             }
+        }
 
-            if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0)
+        foreach (var virtualSeason in virtualSeasons)
+        {
+            var seasonNumber = virtualSeason.IndexNumber;
+            // If there's a physical season with the same number or no episodes in the season, delete it
+            if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
+                || virtualSeason.GetEpisodes().Count == 0)
             {
-                targetItem.AirDays = sourceItem.AirDays;
+                Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+
+                LibraryManager.DeleteItem(
+                    virtualSeason,
+                    new DeleteOptions
+                    {
+                        // Internal metadata paths are removed regardless of this.
+                        DeleteFileLocation = false
+                    },
+                    false);
             }
         }
+    }
 
-        private void RemoveObsoleteSeasons(Series series)
+    private void RemoveObsoleteEpisodes(Series series)
+    {
+        var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
+                        .OfType<Episode>()
+                        .GroupBy(e => e.ParentIndexNumber)
+                        .ToList();
+
+        foreach (var seasonEpisodes in episodesBySeason)
         {
-            // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
-            var physicalSeasonNumbers = new HashSet<int>();
-            var virtualSeasons = new List<Season>();
-            foreach (var existingSeason in series.Children.OfType<Season>())
+            List<Episode> nonPhysicalEpisodes = [];
+            List<Episode> physicalEpisodes = [];
+            foreach (var episode in seasonEpisodes)
             {
-                if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
+                if (episode.IsVirtualItem || episode.IsMissingEpisode)
                 {
-                    physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
-                }
-                else if (existingSeason.LocationType == LocationType.Virtual)
-                {
-                    virtualSeasons.Add(existingSeason);
+                    nonPhysicalEpisodes.Add(episode);
+                    continue;
                 }
+
+                physicalEpisodes.Add(episode);
             }
 
-            foreach (var virtualSeason in virtualSeasons)
+            // Only consider non-physical episodes
+            foreach (var episode in nonPhysicalEpisodes)
             {
-                var seasonNumber = virtualSeason.IndexNumber;
-                // If there's a physical season with the same number or no episodes in the season, delete it
-                if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
-                    || virtualSeason.GetEpisodes().Count == 0)
-                {
-                    Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+                // 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));
 
-                    LibraryManager.DeleteItem(
-                        virtualSeason,
-                        new DeleteOptions
-                        {
-                            // Internal metadata paths are removed regardless of this.
-                            DeleteFileLocation = false
-                        },
-                        false);
+                if (shouldKeep)
+                {
+                    continue;
                 }
+
+                DeleteEpisode(episode);
             }
         }
+    }
 
-        private void RemoveObsoleteEpisodes(Series series)
-        {
-            var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
-                            .OfType<Episode>()
-                            .GroupBy(e => e.ParentIndexNumber)
-                            .ToList();
+    private void DeleteEpisode(Episode episode)
+    {
+        Logger.LogInformation(
+            "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
+            episode.ParentIndexNumber,
+            episode.IndexNumber,
+            episode.SeriesName);
 
-            foreach (var seasonEpisodes in episodesBySeason)
+        LibraryManager.DeleteItem(
+            episode,
+            new DeleteOptions
             {
-                List<Episode> nonPhysicalEpisodes = [];
-                List<Episode> physicalEpisodes = [];
-                foreach (var episode in seasonEpisodes)
-                {
-                    if (episode.IsVirtualItem || episode.IsMissingEpisode)
-                    {
-                        nonPhysicalEpisodes.Add(episode);
-                        continue;
-                    }
+                // Internal metadata paths are removed regardless of this.
+                DeleteFileLocation = false
+            },
+            false);
+    }
 
-                    physicalEpisodes.Add(episode);
-                }
+    /// <summary>
+    /// Creates seasons for all episodes if they don't exist.
+    /// If no season number can be determined, a dummy season will be created.
+    /// </summary>
+    /// <param name="series">The series.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>The async task.</returns>
+    private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
+    {
+        var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
+        var seasons = seriesChildren.OfType<Season>().ToList();
+        var uniqueSeasonNumbers = seriesChildren
+            .OfType<Episode>()
+            .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
+            .Distinct();
 
-                // Only consider non-physical episodes
-                foreach (var episode in nonPhysicalEpisodes)
+        // Loop through the unique season numbers
+        foreach (var seasonNumber in uniqueSeasonNumbers)
+        {
+            // Null season numbers will have a 'dummy' season created because seasons are always required.
+            var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
+            if (existingSeason is null)
+            {
+                var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
+                await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
+            }
+            else if (existingSeason.IsVirtualItem)
+            {
+                var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
+                if (episodeCount > 0)
                 {
-                    // 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(episode);
+                    existingSeason.IsVirtualItem = false;
+                    await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
                 }
             }
         }
+    }
 
-        private void DeleteEpisode(Episode episode)
-        {
-            Logger.LogInformation(
-                "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
-                episode.ParentIndexNumber,
-                episode.IndexNumber,
-                episode.SeriesName);
-
-            LibraryManager.DeleteItem(
-                episode,
-                new DeleteOptions
-                {
-                    // Internal metadata paths are removed regardless of this.
-                    DeleteFileLocation = false
-                },
-                false);
-        }
+    /// <summary>
+    /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
+    /// </summary>
+    /// <param name="series">The series.</param>
+    /// <param name="seasonName">The season name.</param>
+    /// <param name="seasonNumber">The season number.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>The newly created season.</returns>
+    private async Task CreateSeasonAsync(
+        Series series,
+        string? seasonName,
+        int? seasonNumber,
+        CancellationToken cancellationToken)
+    {
+        Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
 
-        /// <summary>
-        /// Creates seasons for all episodes if they don't exist.
-        /// If no season number can be determined, a dummy season will be created.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>The async task.</returns>
-        private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
+        var season = new Season
         {
-            var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
-            var seasons = seriesChildren.OfType<Season>().ToList();
-            var uniqueSeasonNumbers = seriesChildren
-                .OfType<Episode>()
-                .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
-                .Distinct();
+            Name = seasonName,
+            IndexNumber = seasonNumber,
+            Id = LibraryManager.GetNewItemId(
+                series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
+                typeof(Season)),
+            IsVirtualItem = false,
+            SeriesId = series.Id,
+            SeriesName = series.Name,
+            SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
+        };
 
-            // Loop through the unique season numbers
-            foreach (var seasonNumber in uniqueSeasonNumbers)
-            {
-                // Null season numbers will have a 'dummy' season created because seasons are always required.
-                var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
-                if (existingSeason is null)
-                {
-                    var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
-                    await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
-                }
-                else if (existingSeason.IsVirtualItem)
-                {
-                    var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
-                    if (episodeCount > 0)
-                    {
-                        existingSeason.IsVirtualItem = false;
-                        await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-            }
-        }
+        series.AddChild(season);
+        await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+    }
 
-        /// <summary>
-        /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="seasonName">The season name.</param>
-        /// <param name="seasonNumber">The season number.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>The newly created season.</returns>
-        private async Task CreateSeasonAsync(
-            Series series,
-            string? seasonName,
-            int? seasonNumber,
-            CancellationToken cancellationToken)
+    private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
+    {
+        if (string.IsNullOrEmpty(seasonName))
         {
-            Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
-
-            var season = new Season
+            seasonName = seasonNumber switch
             {
-                Name = seasonName,
-                IndexNumber = seasonNumber,
-                Id = LibraryManager.GetNewItemId(
-                    series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
-                    typeof(Season)),
-                IsVirtualItem = false,
-                SeriesId = series.Id,
-                SeriesName = series.Name,
-                SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
+                null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
+                0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
+                _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
             };
-
-            series.AddChild(season);
-            await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
         }
 
-        private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
-        {
-            if (string.IsNullOrEmpty(seasonName))
-            {
-                seasonName = seasonNumber switch
-                {
-                    null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
-                    0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
-                    _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
-                };
-            }
-
-            return seasonName;
-        }
+        return seasonName;
     }
 }

+ 33 - 17
MediaBrowser.Providers/Videos/VideoMetadataService.cs

@@ -1,29 +1,45 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Videos
+namespace MediaBrowser.Providers.Videos;
+
+/// <summary>
+/// Service to manage video metadata.
+/// </summary>
+public class VideoMetadataService : MetadataService<Video, ItemLookupInfo>
 {
-    public class VideoMetadataService : MetadataService<Video, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="VideoMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public VideoMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<VideoMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public VideoMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<VideoMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
-
-        /// <inheritdoc />
-        // Make sure the type-specific services get picked first
-        public override int Order => 10;
     }
+
+    /// <inheritdoc />
+    // Make sure the type-specific services get picked first
+    public override int Order => 10;
 }

+ 29 - 13
MediaBrowser.Providers/Years/YearMetadataService.cs

@@ -1,25 +1,41 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Manager;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Providers.Years
+namespace MediaBrowser.Providers.Years;
+
+/// <summary>
+/// Service to manage year metadata.
+/// </summary>
+public class YearMetadataService : MetadataService<Year, ItemLookupInfo>
 {
-    public class YearMetadataService : MetadataService<Year, ItemLookupInfo>
+    /// <summary>
+    /// Initializes a new instance of the <see cref="YearMetadataService"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="logger">Instance of the <see cref="ILogger"/> 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="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+    /// <param name="keyframeManager">Instance of the <see cref="IKeyframeManager"/> interface.</param>
+    /// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentManager"/> interface.</param>
+    public YearMetadataService(
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<YearMetadataService> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem,
+        ILibraryManager libraryManager,
+        IPathManager pathManager,
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager)
+        : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager)
     {
-        public YearMetadataService(
-            IServerConfigurationManager serverConfigurationManager,
-            ILogger<YearMetadataService> logger,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
-        {
-        }
     }
 }

+ 2 - 2
src/Jellyfin.Drawing/ImageProcessor.cs

@@ -524,11 +524,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
     /// <inheritdoc />
     public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
     {
-        _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
+        _logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
 
         _imageEncoder.CreateImageCollage(options, libraryName);
 
-        _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
+        _logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
     }
 
     /// <inheritdoc />

+ 3 - 2
src/Jellyfin.LiveTv/Channels/ChannelManager.cs

@@ -445,12 +445,13 @@ namespace Jellyfin.LiveTv.Channels
 
             if (item is null)
             {
+                var info = Directory.CreateDirectory(path);
                 item = new Channel
                 {
                     Name = channelInfo.Name,
                     Id = id,
-                    DateCreated = _fileSystem.GetCreationTimeUtc(path),
-                    DateModified = _fileSystem.GetLastWriteTimeUtc(path)
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc
                 };
 
                 isNew = true;

+ 1 - 0
tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs

@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Configuration;