소스 검색

Merge branch 'master' into baseitemkind-fixes

Claus Vium 3 년 전
부모
커밋
663c79cba8
100개의 변경된 파일1922개의 추가작업 그리고 1670개의 파일을 삭제
  1. 1 0
      CONTRIBUTORS.md
  2. 14 12
      Dockerfile
  3. 15 15
      Dockerfile.arm
  4. 15 14
      Dockerfile.arm64
  5. 8 15
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  6. 39 29
      Emby.Dlna/DlnaManager.cs
  7. 4 4
      Emby.Naming/AudioBook/AudioBookInfo.cs
  8. 1 1
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  9. 1 1
      Emby.Naming/Common/NamingOptions.cs
  10. 1 4
      Emby.Naming/Emby.Naming.csproj
  11. 0 1
      Emby.Naming/Video/ExtraResolver.cs
  12. 1 1
      Emby.Naming/Video/VideoListResolver.cs
  13. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  14. 13 19
      Emby.Server.Implementations/Collections/CollectionManager.cs
  15. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  16. 1 1
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  17. 20 14
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  18. 12 10
      Emby.Server.Implementations/Localization/Core/af.json
  19. 1 1
      Emby.Server.Implementations/Localization/Core/ca.json
  20. 7 5
      Emby.Server.Implementations/Localization/Core/el.json
  21. 3 3
      Emby.Server.Implementations/Localization/Core/en-US.json
  22. 10 8
      Emby.Server.Implementations/Localization/Core/es-MX.json
  23. 1 1
      Emby.Server.Implementations/Localization/Core/es.json
  24. 1 1
      Emby.Server.Implementations/Localization/Core/hu.json
  25. 4 4
      Emby.Server.Implementations/Localization/Core/it.json
  26. 1 1
      Emby.Server.Implementations/Localization/Core/kk.json
  27. 1 1
      Emby.Server.Implementations/Localization/Core/ml.json
  28. 1 0
      Emby.Server.Implementations/Localization/Core/pr.json
  29. 1 1
      Emby.Server.Implementations/Localization/Core/ru.json
  30. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  31. 2 1
      Emby.Server.Implementations/Localization/Core/sv.json
  32. 14 12
      Emby.Server.Implementations/Localization/Core/tr.json
  33. 2 2
      Emby.Server.Implementations/Localization/Core/vi.json
  34. 84 100
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  35. 13 24
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  36. 8 3
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  37. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  38. 4 4
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  39. 3 3
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  40. 3 3
      Jellyfin.Server/Jellyfin.Server.csproj
  41. 5 2
      Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
  42. 0 5
      Jellyfin.Server/Middleware/ExceptionMiddleware.cs
  43. 1 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  44. 1 1
      Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
  45. 0 1
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  46. 5 6
      Jellyfin.Server/Program.cs
  47. 1 1
      MediaBrowser.Common/Extensions/ProcessExtensions.cs
  48. 24 24
      MediaBrowser.Common/Net/IPHost.cs
  49. 2 2
      MediaBrowser.Common/Plugins/BasePluginOfT.cs
  50. 4 4
      MediaBrowser.Common/Providers/ProviderIdParsers.cs
  51. 4 4
      MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
  52. 1 3
      MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
  53. 4 5
      MediaBrowser.Controller/Channels/ChannelItemResult.cs
  54. 1 1
      MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
  55. 0 2
      MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
  56. 3 5
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  57. 0 2
      MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
  58. 3 5
      MediaBrowser.Controller/Dlna/IDlnaManager.cs
  59. 21 22
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  60. 17 17
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  61. 1 1
      MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
  62. 26 26
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  63. 32 30
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  64. 18 16
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  65. 1 1
      MediaBrowser.Controller/Entities/AudioBook.cs
  66. 523 381
      MediaBrowser.Controller/Entities/BaseItem.cs
  67. 5 0
      MediaBrowser.Controller/Entities/BaseItemExtensions.cs
  68. 6 6
      MediaBrowser.Controller/Entities/BasePluginFolder.cs
  69. 20 20
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  70. 2 0
      MediaBrowser.Controller/Entities/Extensions.cs
  71. 6 5
      MediaBrowser.Controller/Entities/Folder.cs
  72. 1 1
      MediaBrowser.Controller/Entities/ICollectionFolder.cs
  73. 2 0
      MediaBrowser.Controller/Entities/IHasMediaSources.cs
  74. 1 1
      MediaBrowser.Controller/Entities/IHasShares.cs
  75. 3 0
      MediaBrowser.Controller/Entities/IHasTrailers.cs
  76. 79 79
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  77. 25 25
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  78. 22 20
      MediaBrowser.Controller/Entities/Person.cs
  79. 1 1
      MediaBrowser.Controller/Entities/PersonInfo.cs
  80. 24 24
      MediaBrowser.Controller/Entities/Photo.cs
  81. 18 16
      MediaBrowser.Controller/Entities/Studio.cs
  82. 68 68
      MediaBrowser.Controller/Entities/TV/Episode.cs
  83. 48 44
      MediaBrowser.Controller/Entities/TV/Season.cs
  84. 11 3
      MediaBrowser.Controller/Entities/TV/Series.cs
  85. 4 4
      MediaBrowser.Controller/Entities/Trailer.cs
  86. 7 7
      MediaBrowser.Controller/Entities/UserItemData.cs
  87. 21 21
      MediaBrowser.Controller/Entities/UserRootFolder.cs
  88. 119 119
      MediaBrowser.Controller/Entities/Video.cs
  89. 17 17
      MediaBrowser.Controller/Entities/Year.cs
  90. 9 9
      MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
  91. 49 4
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  92. 218 218
      MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
  93. 7 0
      MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
  94. 43 0
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  95. 8 0
      MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
  96. 16 16
      MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
  97. 0 1
      MediaBrowser.Controller/Persistence/IUserDataRepository.cs
  98. 39 39
      MediaBrowser.Controller/Playlists/Playlist.cs
  99. 1 1
      MediaBrowser.Controller/Providers/IDirectoryService.cs
  100. 10 10
      MediaBrowser.Controller/Resolvers/IItemResolver.cs

+ 1 - 0
CONTRIBUTORS.md

@@ -213,3 +213,4 @@
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
  - [olsh](https://github.com/olsh)
  - [lbenini](https://github.com/lbenini)
+ - [gnuyent](https://github.com/gnuyent)

+ 14 - 12
Dockerfile

@@ -8,15 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
-
-FROM debian:buster-slim
+FROM debian:buster-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -25,9 +17,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
 # https://github.com/intel/compute-runtime/releases
 ARG GMMLIB_VERSION=20.3.2
 ARG IGC_VERSION=1.0.5435
@@ -73,6 +62,19 @@ ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \

+ 15 - 15
Dockerfile.arm

@@ -13,19 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
-
-
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim
+FROM arm32v7/debian:buster-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -61,14 +50,25 @@ RUN apt-get update \
  && chmod 777 /cache /config /media \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \

+ 15 - 14
Dockerfile.arm64

@@ -13,18 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
-
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim
+FROM arm64v8/debian:buster-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -50,14 +40,25 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
  && chmod 777 /cache /config /media \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \

+ 8 - 15
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -288,21 +288,14 @@ namespace Emby.Dlna.ContentDirectory
         /// <returns>The xml feature list.</returns>
         private static string WriteFeatureListXml()
         {
-            // TODO: clean this up
-            var builder = new StringBuilder();
-
-            builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
-            builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
-
-            builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
-            builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
-            builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
-            builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
-            builder.Append("</Feature>");
-
-            builder.Append("</Features>");
-
-            return builder.ToString();
+            return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
+                + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
+                + "<container id=\"I\" type=\"object.item.imageItem\"/>"
+                + "<container id=\"A\" type=\"object.item.audioItem\"/>"
+                + "<container id=\"V\" type=\"object.item.videoItem\"/>"
+                + "</Feature>"
+                + "</Features>";
         }
 
         /// <summary>

+ 39 - 29
Emby.Dlna/DlnaManager.cs

@@ -1,7 +1,4 @@
-#nullable disable
-
 #pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -96,12 +93,14 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public DeviceProfile GetDefaultProfile()
         {
             return new DefaultProfile();
         }
 
-        public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
         {
             if (deviceInfo == null)
             {
@@ -111,13 +110,13 @@ namespace Emby.Dlna
             var profile = GetProfiles()
                 .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
 
-            if (profile != null)
+            if (profile == null)
             {
-                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
+                LogUnmatchedProfile(deviceInfo);
             }
             else
             {
-                LogUnmatchedProfile(deviceInfo);
+                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
             }
 
             return profile;
@@ -187,7 +186,8 @@ namespace Emby.Dlna
             }
         }
 
-        public DeviceProfile GetProfile(IHeaderDictionary headers)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(IHeaderDictionary headers)
         {
             if (headers == null)
             {
@@ -195,15 +195,13 @@ namespace Emby.Dlna
             }
 
             var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
-
-            if (profile != null)
+            if (profile == null)
             {
-                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+                _logger.LogDebug("No matching device profile found. {@Headers}", headers);
             }
             else
             {
-                var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
-                _logger.LogDebug("No matching device profile found. {0}", headerString);
+                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
             }
 
             return profile;
@@ -253,19 +251,19 @@ namespace Emby.Dlna
                 return xmlFies
                     .Select(i => ParseProfileFile(i, type))
                     .Where(i => i != null)
-                    .ToList();
+                    .ToList()!; // We just filtered out all the nulls
             }
             catch (IOException)
             {
-                return new List<DeviceProfile>();
+                return Array.Empty<DeviceProfile>();
             }
         }
 
-        private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
+        private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
         {
             lock (_profiles)
             {
-                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
+                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
                 {
                     return profileTuple.Item2;
                 }
@@ -293,7 +291,8 @@ namespace Emby.Dlna
             }
         }
 
-        public DeviceProfile GetProfile(string id)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(string id)
         {
             if (string.IsNullOrEmpty(id))
             {
@@ -322,6 +321,7 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public IEnumerable<DeviceProfileInfo> GetProfileInfos()
         {
             return GetProfileInfosInternal().Select(i => i.Info);
@@ -329,17 +329,14 @@ namespace Emby.Dlna
 
         private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
         {
-            return new InternalProfileInfo
-            {
-                Path = file.FullName,
-
-                Info = new DeviceProfileInfo
+            return new InternalProfileInfo(
+                new DeviceProfileInfo
                 {
                     Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
                     Name = _fileSystem.GetFileNameWithoutExtension(file),
                     Type = type
-                }
-            };
+                },
+                file.FullName);
         }
 
         private async Task ExtractSystemProfilesAsync()
@@ -359,7 +356,8 @@ namespace Emby.Dlna
                     systemProfilesPath,
                     Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
 
-                using (var stream = _assembly.GetManifestResourceStream(name))
+                // The stream should exist as we just got its name from GetManifestResourceNames
+                using (var stream = _assembly.GetManifestResourceStream(name)!)
                 {
                     var fileInfo = _fileSystem.GetFileInfo(path);
 
@@ -380,6 +378,7 @@ namespace Emby.Dlna
             Directory.CreateDirectory(UserProfilesPath);
         }
 
+        /// <inheritdoc />
         public void DeleteProfile(string id)
         {
             var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@@ -397,6 +396,7 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public void CreateProfile(DeviceProfile profile)
         {
             profile = ReserializeProfile(profile);
@@ -412,6 +412,7 @@ namespace Emby.Dlna
             SaveProfile(profile, path, DeviceProfileType.User);
         }
 
+        /// <inheritdoc />
         public void UpdateProfile(DeviceProfile profile)
         {
             profile = ReserializeProfile(profile);
@@ -470,9 +471,11 @@ namespace Emby.Dlna
 
             var json = JsonSerializer.Serialize(profile, _jsonOptions);
 
-            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
+            // Output can't be null if the input isn't null
+            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
         }
 
+        /// <inheritdoc />
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
             var profile = GetDefaultProfile();
@@ -482,6 +485,7 @@ namespace Emby.Dlna
             return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
         }
 
+        /// <inheritdoc />
         public ImageStream GetIcon(string filename)
         {
             var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
@@ -499,9 +503,15 @@ namespace Emby.Dlna
 
         private class InternalProfileInfo
         {
-            internal DeviceProfileInfo Info { get; set; }
+            internal InternalProfileInfo(DeviceProfileInfo info, string path)
+            {
+                Info = info;
+                Path = path;
+            }
+
+            internal DeviceProfileInfo Info { get; }
 
-            internal string Path { get; set; }
+            internal string Path { get; }
         }
     }
 

+ 4 - 4
Emby.Naming/AudioBook/AudioBookInfo.cs

@@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
         /// <param name="files">List of files composing the actual audiobook.</param>
         /// <param name="extras">List of extra files.</param>
         /// <param name="alternateVersions">Alternative version of files.</param>
-        public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
+        public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
         {
             Name = name;
             Year = year;
@@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
         /// Gets or sets the files.
         /// </summary>
         /// <value>The files.</value>
-        public List<AudioBookFileInfo> Files { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
 
         /// <summary>
         /// Gets or sets the extras.
         /// </summary>
         /// <value>The extras.</value>
-        public List<AudioBookFileInfo> Extras { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
 
         /// <summary>
         /// Gets or sets the alternate versions.
         /// </summary>
         /// <value>The alternate versions.</value>
-        public List<AudioBookFileInfo> AlternateVersions { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
     }
 }

+ 1 - 1
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
                         foreach (var audioFile in group)
                         {
                             var name = Path.GetFileNameWithoutExtension(audioFile.Path);
-                            if (name.Equals("audiobook") ||
+                            if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
                                 name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
                                 name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
                             {

+ 1 - 1
Emby.Naming/Common/NamingOptions.cs

@@ -284,7 +284,7 @@ namespace Emby.Naming.Common
 
                 // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
                 // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
+                new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
                 {
                     IsNamed = true
                 },

+ 1 - 4
Emby.Naming/Emby.Naming.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
   <PropertyGroup>
@@ -49,7 +49,4 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-  </PropertyGroup>
-
 </Project>

+ 0 - 1
Emby.Naming/Video/ExtraResolver.cs

@@ -1,6 +1,5 @@
 using System;
 using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Audio;
 using Emby.Naming.Common;

+ 1 - 1
Emby.Naming/Video/VideoListResolver.cs

@@ -21,7 +21,7 @@ namespace Emby.Naming.Video
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
-        public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+        public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
         {
             var videoInfos = files
                 .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -880,7 +880,7 @@ namespace Emby.Server.Implementations.Channels
             }
         }
 
-        private async Task CacheResponse(object result, string path)
+        private async Task CacheResponse(ChannelItemResult result, string path)
         {
             try
             {

+ 13 - 19
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
         }
 
         /// <inheritdoc />
-        public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+        public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 
         private IEnumerable<Folder> FindFolders(string path)
         {
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Collections
                 .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
         }
 
-        internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
+        internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
         {
             var existingFolder = FindFolders(path).FirstOrDefault();
             if (existingFolder != null)
@@ -114,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
             return Path.Combine(_appPaths.DataPath, "collections");
         }
 
-        private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
+        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }
@@ -203,8 +201,7 @@ namespace Emby.Server.Implementations.Collections
 
         private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
@@ -256,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
         /// <inheritdoc />
         public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
@@ -312,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
 
             foreach (var item in items)
             {
-                if (item is not ISupportsBoxSetGrouping)
-                {
-                    results[item.Id] = item;
-                }
-                else
+                if (item is ISupportsBoxSetGrouping)
                 {
                     var itemId = item.Id;
 
@@ -340,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
                     }
 
                     var alreadyInResults = false;
+
                     // this is kind of a performance hack because only Video has alternate versions that should be in a box set?
                     if (item is Video video)
                     {
@@ -355,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
                         }
                     }
 
-                    if (!alreadyInResults)
+                    if (alreadyInResults)
                     {
-                        results[itemId] = item;
+                        continue;
                     }
                 }
+
+                results[item.Id] = item;
             }
 
             return results.Values;

+ 1 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -28,7 +28,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.8" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
     <PackageReference Include="sharpcompress" Version="0.28.3" />

+ 1 - 1
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 }
 
                 // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+                var allowTokenInfoUpdate = authInfo.Client == null || !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
 
                 if (string.IsNullOrWhiteSpace(authInfo.Device))
                 {

+ 20 - 14
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -13,7 +13,6 @@ using System.Threading.Tasks;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using Microsoft.Extensions.Logging;
@@ -44,22 +43,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
         {
-            if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            if (info == null)
             {
-                using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
-                if (!string.IsNullOrEmpty(info.UserAgent))
-                {
-                    requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
-                }
+                throw new ArgumentNullException(nameof(info));
+            }
+
+            if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                return File.OpenRead(info.Url);
+            }
 
-                var response = await _httpClientFactory.CreateClient(NamedClient.Default)
-                    .SendAsync(requestMessage, cancellationToken)
-                    .ConfigureAwait(false);
-                response.EnsureSuccessStatusCode();
-                return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+            if (!string.IsNullOrEmpty(info.UserAgent))
+            {
+                requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
             }
 
-            return File.OpenRead(info.Url);
+            // Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
+            var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                .ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
+
+            return await response.Content.ReadAsStreamAsync(cancellationToken);
         }
 
         private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
@@ -83,7 +89,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
                 {
                     extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
-                    _logger.LogInformation("Found m3u channel: {0}", extInf);
                 }
                 else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
                 {
@@ -99,6 +104,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                     channel.Path = trimmedLine;
                     channels.Add(channel);
+                    _logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
                     extInf = string.Empty;
                 }
             }

+ 12 - 10
Emby.Server.Implementations/Localization/Core/af.json

@@ -2,24 +2,24 @@
     "Artists": "Kunstenare",
     "Channels": "Kanale",
     "Folders": "Lêergidse",
-    "Favorites": "Gunstellinge",
+    "Favorites": "Gunstelinge",
     "HeaderFavoriteShows": "Gunsteling Vertonings",
     "ValueSpecialEpisodeName": "Spesiale - {0}",
-    "HeaderAlbumArtists": "Album Kunstenaars",
+    "HeaderAlbumArtists": "Kunstenaars se Album",
     "Books": "Boeke",
     "HeaderNextUp": "Volgende",
     "Movies": "Flieks",
     "Shows": "Televisie Reekse",
     "HeaderContinueWatching": "Kyk Verder",
     "HeaderFavoriteEpisodes": "Gunsteling Episodes",
-    "Photos": "Fotos",
+    "Photos": "Foto's",
     "Playlists": "Snitlyste",
     "HeaderFavoriteArtists": "Gunsteling Kunstenaars",
     "HeaderFavoriteAlbums": "Gunsteling Albums",
     "Sync": "Sinkroniseer",
     "HeaderFavoriteSongs": "Gunsteling Liedjies",
     "Songs": "Liedjies",
-    "DeviceOnlineWithName": "{0} gekoppel is",
+    "DeviceOnlineWithName": "{0} is gekoppel",
     "DeviceOfflineWithName": "{0} is ontkoppel",
     "Collections": "Versamelings",
     "Inherit": "Ontvang",
@@ -71,7 +71,7 @@
     "NameSeasonUnknown": "Seisoen Onbekend",
     "NameSeasonNumber": "Seisoen {0}",
     "NameInstallFailed": "{0} installering het misluk",
-    "MusicVideos": "Musiek videos",
+    "MusicVideos": "Musiek Videos",
     "Music": "Musiek",
     "MixedContent": "Gemengde inhoud",
     "MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
@@ -79,15 +79,15 @@
     "MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
     "MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
     "Latest": "Nuutste",
-    "LabelRunningTimeValue": "Lopende tyd: {0}",
+    "LabelRunningTimeValue": "Werktyd: {0}",
     "LabelIpAddressValue": "IP adres: {0}",
     "ItemRemovedWithName": "{0} is uit versameling verwyder",
-    "ItemAddedWithName": "{0} is in die versameling",
-    "HomeVideos": "Tuis opnames",
+    "ItemAddedWithName": "{0} is by die versameling gevoeg",
+    "HomeVideos": "Tuis Videos",
     "HeaderRecordingGroups": "Groep Opnames",
     "Genres": "Genres",
     "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
-    "ChapterNameValue": "Hoofstuk",
+    "ChapterNameValue": "Hoofstuk {0}",
     "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
     "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
     "Albums": "Albums",
@@ -117,5 +117,7 @@
     "Forced": "Geforseer",
     "Default": "Oorspronklik",
     "TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
-    "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
+    "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
+    "TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
+    "TaskOptimizeDatabase": "Optimaliseer databasis"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ca.json

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "Books": "Llibres",
-    "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
+    "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
     "Channels": "Canals",
     "ChapterNameValue": "Capítol {0}",
     "Collections": "Col·leccions",

+ 7 - 5
Emby.Server.Implementations/Localization/Core/el.json

@@ -1,5 +1,5 @@
 {
-    "Albums": "Άλμπουμς",
+    "Albums": "Άλμπουμ",
     "AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
     "Application": "Εφαρμογή",
     "Artists": "Καλλιτέχνες",
@@ -15,7 +15,7 @@
     "Favorites": "Αγαπημένα",
     "Folders": "Φάκελοι",
     "Genres": "Είδη",
-    "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
+    "HeaderAlbumArtists": "Άλμπουμ Καλλιτέχνη",
     "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
     "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
     "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@@ -39,7 +39,7 @@
     "MixedContent": "Ανάμεικτο Περιεχόμενο",
     "Movies": "Ταινίες",
     "Music": "Μουσική",
-    "MusicVideos": "Μουσικά βίντεο",
+    "MusicVideos": "Μουσικά Βίντεο",
     "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
     "NameSeasonNumber": "Κύκλος {0}",
     "NameSeasonUnknown": "Άγνωστος Κύκλος",
@@ -62,7 +62,7 @@
     "NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
     "Photos": "Φωτογραφίες",
     "Playlists": "Λίστες αναπαραγωγής",
-    "Plugin": "Plugin",
+    "Plugin": "Πρόσθετο",
     "PluginInstalledWithName": "{0} εγκαταστήθηκε",
     "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
     "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
     "Undefined": "Απροσδιόριστο",
     "Forced": "Εξαναγκασμένο",
-    "Default": "Προεπιλογή"
+    "Default": "Προεπιλογή",
+    "TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.",
+    "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων"
 }

+ 3 - 3
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -17,7 +17,7 @@
     "Folders": "Folders",
     "Forced": "Forced",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Album Artists",
+    "HeaderAlbumArtists": "Artist's Album",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favorite Albums",
     "HeaderFavoriteArtists": "Favorite Artists",
@@ -27,7 +27,7 @@
     "HeaderLiveTV": "Live TV",
     "HeaderNextUp": "Next Up",
     "HeaderRecordingGroups": "Recording Groups",
-    "HomeVideos": "Home videos",
+    "HomeVideos": "Home Videos",
     "Inherit": "Inherit",
     "ItemAddedWithName": "{0} was added to the library",
     "ItemRemovedWithName": "{0} was removed from the library",
@@ -41,7 +41,7 @@
     "MixedContent": "Mixed content",
     "Movies": "Movies",
     "Music": "Music",
-    "MusicVideos": "Music videos",
+    "MusicVideos": "Music Videos",
     "NameInstallFailed": "{0} installation failed",
     "NameSeasonNumber": "Season {0}",
     "NameSeasonUnknown": "Season Unknown",

+ 10 - 8
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -15,7 +15,7 @@
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderAlbumArtists": "Artistas del Álbum",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "TV en vivo",
     "HeaderNextUp": "A continuación",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "HomeVideos": "Videos caseros",
+    "HomeVideos": "Videos Caseros",
     "Inherit": "Heredar",
     "ItemAddedWithName": "{0} fue agregado a la biblioteca",
     "ItemRemovedWithName": "{0} fue removido de la biblioteca",
@@ -39,7 +39,7 @@
     "MixedContent": "Contenido mezclado",
     "Movies": "Películas",
     "Music": "Música",
-    "MusicVideos": "Videos musicales",
+    "MusicVideos": "Videos Musicales",
     "NameInstallFailed": "Falló la instalación de {0}",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonUnknown": "Temporada desconocida",
@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
     "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
     "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
-    "NotificationOptionInstallationFailed": "Falla de instalación",
+    "NotificationOptionInstallationFailed": "Fallo en la instalación",
     "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
     "NotificationOptionPluginError": "Falla de complemento",
     "NotificationOptionPluginInstalled": "Complemento instalado",
@@ -69,7 +69,7 @@
     "ProviderValue": "Proveedor: {0}",
     "ScheduledTaskFailedWithName": "{0} falló",
     "ScheduledTaskStartedWithName": "{0} iniciado",
-    "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+    "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
     "Shows": "Programas",
     "Songs": "Canciones",
     "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
@@ -94,9 +94,9 @@
     "VersionNumber": "Versión {0}",
     "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
     "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
-    "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+    "TaskRefreshChannelsDescription": "Actualiza la información de los canales de Internet.",
     "TaskRefreshChannels": "Actualizar canales",
-    "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+    "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día de antigüedad.",
     "TaskCleanTranscode": "Limpiar directorio de transcodificado",
     "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
     "TaskUpdatePlugins": "Actualizar complementos",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Limpiar registro de actividades",
     "Undefined": "Sin definir",
     "Forced": "Forzado",
-    "Default": "Predeterminado"
+    "Default": "Predeterminado",
+    "TaskOptimizeDatabase": "Optimizar base de datos",
+    "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos."
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es.json

@@ -15,7 +15,7 @@
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderAlbumArtists": "Artista del álbum",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -15,7 +15,7 @@
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
-    "HeaderAlbumArtists": "Album előadók",
+    "HeaderAlbumArtists": "Előadó albumai",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",

+ 4 - 4
Emby.Server.Implementations/Localization/Core/it.json

@@ -15,7 +15,7 @@
     "Favorites": "Preferiti",
     "Folders": "Cartelle",
     "Genres": "Generi",
-    "HeaderAlbumArtists": "Artisti degli Album",
+    "HeaderAlbumArtists": "Artisti dell'Album",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Diretta TV",
     "HeaderNextUp": "Prossimo",
     "HeaderRecordingGroups": "Gruppi di Registrazione",
-    "HomeVideos": "Video personali",
+    "HomeVideos": "Video Personali",
     "Inherit": "Eredita",
     "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
     "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
@@ -39,7 +39,7 @@
     "MixedContent": "Contenuto misto",
     "Movies": "Film",
     "Music": "Musica",
-    "MusicVideos": "Video musicali",
+    "MusicVideos": "Video Musicali",
     "NameInstallFailed": "{0} installazione fallita",
     "NameSeasonNumber": "Stagione {0}",
     "NameSeasonUnknown": "Stagione sconosciuta",
@@ -70,7 +70,7 @@
     "ScheduledTaskFailedWithName": "{0} fallito",
     "ScheduledTaskStartedWithName": "{0} avviati",
     "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
-    "Shows": "Programmi",
+    "Shows": "Serie TV",
     "Songs": "Canzoni",
     "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
     "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/kk.json

@@ -15,7 +15,7 @@
     "Favorites": "Tañdaulylar",
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
-    "HeaderAlbumArtists": "Älbom oryndauşylary",
+    "HeaderAlbumArtists": "Oryndauşynyñ älbomy",
     "HeaderContinueWatching": "Qaraudy jalğastyru",
     "HeaderFavoriteAlbums": "Tañdauly älbomdar",
     "HeaderFavoriteArtists": "Tañdauly oryndauşylar",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ml.json

@@ -103,7 +103,7 @@
     "ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
     "Collections": "ശേഖരങ്ങൾ",
     "Folders": "ഫോൾഡറുകൾ",
-    "HeaderAlbumArtists": "ആൽബം ആർട്ടിസ്റ്റുകൾ",
+    "HeaderAlbumArtists": "കലാകാരന്റെ ആൽബം",
     "Sync": "സമന്വയിപ്പിക്കുക",
     "Movies": "സിനിമകൾ",
     "Photos": "ഫോട്ടോകൾ",

+ 1 - 0
Emby.Server.Implementations/Localization/Core/pr.json

@@ -0,0 +1 @@
+{}

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ru.json

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Эфир",
     "HeaderNextUp": "Очередное",
     "HeaderRecordingGroups": "Группы записей",
-    "HomeVideos": "Домашнее видео",
+    "HomeVideos": "Домашние видео",
     "Inherit": "Наследуемое",
     "ItemAddedWithName": "{0} - добавлено в медиатеку",
     "ItemRemovedWithName": "{0} - изъято из медиатеки",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/sk.json

@@ -39,7 +39,7 @@
     "MixedContent": "Zmiešaný obsah",
     "Movies": "Filmy",
     "Music": "Hudba",
-    "MusicVideos": "Hudobné videoklipy",
+    "MusicVideos": "Hudobné videá",
     "NameInstallFailed": "Inštalácia {0} zlyhala",
     "NameSeasonNumber": "Séria {0}",
     "NameSeasonUnknown": "Neznáma séria",

+ 2 - 1
Emby.Server.Implementations/Localization/Core/sv.json

@@ -118,5 +118,6 @@
     "TaskCleanActivityLog": "Rensa Aktivitets Logg",
     "Undefined": "odefinierad",
     "Forced": "Tvingad",
-    "Default": "Standard"
+    "Default": "Standard",
+    "TaskOptimizeDatabase": "Optimera databasen"
 }

+ 14 - 12
Emby.Server.Implementations/Localization/Core/tr.json

@@ -43,7 +43,7 @@
     "NameInstallFailed": "{0} kurulumu başarısız",
     "NameSeasonNumber": "Sezon {0}",
     "NameSeasonUnknown": "Bilinmeyen Sezon",
-    "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.",
+    "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
     "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
     "NotificationOptionApplicationUpdateInstalled": "Uygulama güncellemesi yüklendi",
     "NotificationOptionAudioPlayback": "Ses çalma başladı",
@@ -75,7 +75,7 @@
     "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
-    "Sync": "Eşitle",
+    "Sync": "Eşzamanlama",
     "System": "Sistem",
     "TvShows": "Diziler",
     "User": "Kullanıcı",
@@ -89,34 +89,36 @@
     "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
     "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
     "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
-    "ValueHasBeenAddedToLibrary": "Medya kitaplığınıza {0} eklendi",
+    "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
     "ValueSpecialEpisodeName": "Özel - {0}",
-    "VersionNumber": "Versiyon {0}",
+    "VersionNumber": "Sürüm {0}",
     "TaskCleanCache": "Geçici dosya klasörünü temizle",
     "TasksChannelsCategory": "İnternet kanalları",
     "TasksApplicationCategory": "Uygulama",
     "TasksLibraryCategory": "Kütüphane",
-    "TasksMaintenanceCategory": "Onarım",
+    "TasksMaintenanceCategory": "Bakım",
     "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
     "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
     "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
     "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
     "TaskRefreshChannels": "Kanalları Yenile",
-    "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.",
+    "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
     "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
     "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
     "TaskUpdatePlugins": "Eklentileri Güncelle",
     "TaskRefreshPeople": "Kullanıcıları Yenile",
-    "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.",
-    "TaskCleanLogs": "Log Dizinini Temizle",
-    "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.",
+    "TaskCleanLogsDescription": "{0} günden eski günlük dosyalarını siler.",
+    "TaskCleanLogs": "Günlük Dizinini Temizle",
+    "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.",
     "TaskRefreshLibrary": "Medya Kütüphanesini Tara",
     "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
     "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
     "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
-    "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
-    "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
+    "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",
+    "TaskCleanActivityLogDescription": "Yapılandırılan tarihten daha eski olan etkinlik günlüğü girişlerini siler.",
     "Undefined": "Bilinmeyen",
     "Default": "Varsayılan",
-    "Forced": "Zorla"
+    "Forced": "Zorla",
+    "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.",
+    "TaskOptimizeDatabase": "Veritabanını optimize et"
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/vi.json

@@ -3,7 +3,7 @@
     "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
-    "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
+    "HeaderAlbumArtists": "Album Nghệ sĩ",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
@@ -82,7 +82,7 @@
     "NameSeasonUnknown": "Không Rõ Mùa",
     "NameSeasonNumber": "Phần {0}",
     "NameInstallFailed": "{0} cài đặt thất bại",
-    "MusicVideos": "Video Nhạc",
+    "MusicVideos": "Videos Nhạc",
     "Music": "Nhạc",
     "MixedContent": "Nội dung hỗn hợp",
     "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",

+ 84 - 100
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -23,6 +21,9 @@ namespace Emby.Server.Implementations.Localization
     public class LocalizationManager : ILocalizationManager
     {
         private const string DefaultCulture = "en-US";
+        private const string RatingsPath = "Emby.Server.Implementations.Localization.Ratings.";
+        private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
+        private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
         private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
         private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
 
@@ -35,10 +36,10 @@ namespace Emby.Server.Implementations.Localization
         private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
             new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
 
-        private List<CultureDto> _cultures;
-
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
+        private List<CultureDto> _cultures = new List<CultureDto>();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.
         /// </summary>
@@ -58,43 +59,39 @@ namespace Emby.Server.Implementations.Localization
         /// <returns><see cref="Task" />.</returns>
         public async Task LoadAll()
         {
-            const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings.";
-
             // Extract from the assembly
             foreach (var resource in _assembly.GetManifestResourceNames())
             {
-                if (!resource.StartsWith(RatingsResource, StringComparison.Ordinal))
+                if (!resource.StartsWith(RatingsPath, StringComparison.Ordinal))
                 {
                     continue;
                 }
 
-                string countryCode = resource.Substring(RatingsResource.Length, 2);
+                string countryCode = resource.Substring(RatingsPath.Length, 2);
                 var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
 
-                using (var str = _assembly.GetManifestResourceStream(resource))
-                using (var reader = new StreamReader(str))
+                await using var stream = _assembly.GetManifestResourceStream(resource);
+                using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
+                await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
                 {
-                    await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+                    if (string.IsNullOrWhiteSpace(line))
                     {
-                        if (string.IsNullOrWhiteSpace(line))
-                        {
-                            continue;
-                        }
-
-                        string[] parts = line.Split(',');
-                        if (parts.Length == 2
-                            && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
-                        {
-                            var name = parts[0];
-                            dict.Add(name, new ParentalRating(name, value));
-                        }
+                        continue;
+                    }
+
+                    string[] parts = line.Split(',');
+                    if (parts.Length == 2
+                        && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+                    {
+                        var name = parts[0];
+                        dict.Add(name, new ParentalRating(name, value));
+                    }
 #if DEBUG
-                        else
-                        {
-                            _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
-                        }
-#endif
+                    else
+                    {
+                        _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
                     }
+#endif
                 }
 
                 _allParentalRatings[countryCode] = dict;
@@ -114,52 +111,49 @@ namespace Emby.Server.Implementations.Localization
         {
             List<CultureDto> list = new List<CultureDto>();
 
-            const string ResourcePath = "Emby.Server.Implementations.Localization.iso6392.txt";
-
-            using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
-            using (var reader = new StreamReader(stream))
+            await using var stream = _assembly.GetManifestResourceStream(CulturesPath)
+                ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
+            using var reader = new StreamReader(stream);
+            await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
             {
-                await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+                if (string.IsNullOrWhiteSpace(line))
                 {
-                    if (string.IsNullOrWhiteSpace(line))
+                    continue;
+                }
+
+                var parts = line.Split('|');
+
+                if (parts.Length == 5)
+                {
+                    string name = parts[3];
+                    if (string.IsNullOrWhiteSpace(name))
                     {
                         continue;
                     }
 
-                    var parts = line.Split('|');
+                    string twoCharName = parts[2];
+                    if (string.IsNullOrWhiteSpace(twoCharName))
+                    {
+                        continue;
+                    }
 
-                    if (parts.Length == 5)
+                    string[] threeletterNames;
+                    if (string.IsNullOrWhiteSpace(parts[1]))
                     {
-                        string name = parts[3];
-                        if (string.IsNullOrWhiteSpace(name))
-                        {
-                            continue;
-                        }
-
-                        string twoCharName = parts[2];
-                        if (string.IsNullOrWhiteSpace(twoCharName))
-                        {
-                            continue;
-                        }
-
-                        string[] threeletterNames;
-                        if (string.IsNullOrWhiteSpace(parts[1]))
-                        {
-                            threeletterNames = new[] { parts[0] };
-                        }
-                        else
-                        {
-                            threeletterNames = new[] { parts[0], parts[1] };
-                        }
-
-                        list.Add(new CultureDto
-                        {
-                            DisplayName = name,
-                            Name = name,
-                            ThreeLetterISOLanguageNames = threeletterNames,
-                            TwoLetterISOLanguageName = twoCharName
-                        });
+                        threeletterNames = new[] { parts[0] };
                     }
+                    else
+                    {
+                        threeletterNames = new[] { parts[0], parts[1] };
+                    }
+
+                    list.Add(new CultureDto
+                    {
+                        DisplayName = name,
+                        Name = name,
+                        ThreeLetterISOLanguageNames = threeletterNames,
+                        TwoLetterISOLanguageName = twoCharName
+                    });
                 }
             }
 
@@ -167,7 +161,7 @@ namespace Emby.Server.Implementations.Localization
         }
 
         /// <inheritdoc />
-        public CultureDto FindLanguageInfo(string language)
+        public CultureDto? FindLanguageInfo(string language)
         {
             // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
             for (var i = 0; i < _cultures.Count; i++)
@@ -188,9 +182,10 @@ namespace Emby.Server.Implementations.Localization
         /// <inheritdoc />
         public IEnumerable<CountryInfo> GetCountries()
         {
-            using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
-
-            return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
+            using StreamReader reader = new StreamReader(
+                _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"));
+            return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions)
+                ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'");
         }
 
         /// <inheritdoc />
@@ -210,7 +205,9 @@ namespace Emby.Server.Implementations.Localization
                 countryCode = "us";
             }
 
-            return GetRatings(countryCode) ?? GetRatings("us");
+            return GetRatings(countryCode)
+                ?? GetRatings("us")
+                ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
         }
 
         /// <summary>
@@ -218,7 +215,7 @@ namespace Emby.Server.Implementations.Localization
         /// </summary>
         /// <param name="countryCode">The country code.</param>
         /// <returns>The ratings.</returns>
-        private Dictionary<string, ParentalRating> GetRatings(string countryCode)
+        private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
         {
             _allParentalRatings.TryGetValue(countryCode, out var value);
 
@@ -243,7 +240,7 @@ namespace Emby.Server.Implementations.Localization
 
             var ratingsDictionary = GetParentalRatingsDictionary();
 
-            if (ratingsDictionary.TryGetValue(rating, out ParentalRating value))
+            if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
             {
                 return value.Value;
             }
@@ -273,20 +270,6 @@ namespace Emby.Server.Implementations.Localization
             return null;
         }
 
-        /// <inheritdoc />
-        public bool HasUnicodeCategory(string value, UnicodeCategory category)
-        {
-            foreach (var chr in value)
-            {
-                if (char.GetUnicodeCategory(chr) == category)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
         /// <inheritdoc />
         public string GetLocalizedString(string phrase)
         {
@@ -350,22 +333,23 @@ namespace Emby.Server.Implementations.Localization
 
         private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
         {
-            using (var stream = _assembly.GetManifestResourceStream(resourcePath))
+            await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+            // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
+            if (stream == null)
             {
-                // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
-                if (stream != null)
-                {
-                    var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+                _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+                return;
+            }
 
-                    foreach (var key in dict.Keys)
-                    {
-                        dictionary[key] = dict[key];
-                    }
-                }
-                else
-                {
-                    _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
-                }
+            var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+            if (dict == null)
+            {
+                throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
+            }
+
+            foreach (var key in dict.Keys)
+            {
+                dictionary[key] = dict[key];
             }
         }
 

+ 13 - 24
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -55,9 +55,19 @@ namespace Emby.Server.Implementations.ScheduledTasks
             _localization = localization;
         }
 
-        /// <summary>
-        /// Creates the triggers that define when the task will run.
-        /// </summary>
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+        /// <inheritdoc />
+        public string Key => "RefreshChapterImages";
+
+        /// <inheritdoc />
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
             return new[]
@@ -162,26 +172,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 }
             }
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
-        /// <inheritdoc />
-        public string Key => "RefreshChapterImages";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 8 - 3
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -380,7 +380,7 @@ namespace Jellyfin.Api.Helpers
         private void DeleteHlsPartialStreamFiles(string outputFilePath)
         {
             var directory = Path.GetDirectoryName(outputFilePath)
-                ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+                            ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
 
             var name = Path.GetFileNameWithoutExtension(outputFilePath);
 
@@ -444,6 +444,10 @@ namespace Jellyfin.Api.Helpers
             {
                 var audioCodec = state.ActualOutputAudioCodec;
                 var videoCodec = state.ActualOutputVideoCodec;
+                var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+                HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString)
+                    ? null
+                    : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true);
 
                 _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
                 {
@@ -458,6 +462,7 @@ namespace Jellyfin.Api.Helpers
                     AudioChannels = state.OutputAudioChannels,
                     IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
                     IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+                    HardwareAccelerationType = hardwareAccelerationType,
                     TranscodeReasons = state.TranscodeReasons
                 });
             }
@@ -759,8 +764,8 @@ namespace Jellyfin.Api.Helpers
             if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
             {
                 var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
-                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
-                    cancellationTokenSource.Token)
+                        new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+                        cancellationTokenSource.Token)
                     .ConfigureAwait(false);
                 var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -14,7 +14,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.8" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.9" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.5" />

+ 4 - 4
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -19,13 +19,13 @@
 
   <ItemGroup>
     <PackageReference Include="System.Linq.Async" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.8" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.8">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 3 - 3
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -303,7 +303,7 @@ namespace Jellyfin.Server.Extensions
                     {
                         description.TryGetMethodInfo(out MethodInfo methodInfo);
                         // Attribute name, method name, none.
-                        return description?.ActionDescriptor?.AttributeRouteInfo?.Name
+                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
                                ?? methodInfo?.Name
                                ?? null;
                     });
@@ -341,7 +341,7 @@ namespace Jellyfin.Server.Extensions
                 {
                     foreach (var address in host.GetAddresses())
                     {
-                        AddIpAddress(config, options, addr.Address, addr.PrefixLength);
+                        AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
                     }
                 }
             }
@@ -397,7 +397,7 @@ namespace Jellyfin.Server.Extensions
                     Type = "object",
                     Properties = typeof(ImageType).GetEnumNames().ToDictionary(
                         name => name,
-                        name => new OpenApiSchema
+                        _ => new OpenApiSchema
                         {
                             Type = "object",
                             AdditionalProperties = new OpenApiSchema

+ 3 - 3
Jellyfin.Server/Jellyfin.Server.csproj

@@ -33,13 +33,13 @@
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.8" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.8" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.9" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.9" />
     <PackageReference Include="prometheus-net" Version="4.2.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="4.2.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
     <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
-    <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
+    <PackageReference Include="Serilog.Settings.Configuration" Version="3.2.0" />
     <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
     <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

+ 5 - 2
Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -58,9 +58,12 @@ namespace Jellyfin.Server.Middleware
                     return;
                 }
 
-                if (!startsWithBaseUrl)
+                if (!startsWithBaseUrl
+                    || localPath.Length == baseUrlPrefix.Length
+                    // Local path is /baseUrl/
+                    || (localPath.Length == baseUrlPrefix.Length + 1 && localPath[^1] == '/'))
                 {
-                    // Always redirect back to the default path if the base prefix is invalid or missing
+                    // Always redirect back to the default path if the base prefix is invalid, missing, or is the full path.
                     _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
                     httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
                     return;

+ 0 - 5
Jellyfin.Server/Middleware/ExceptionMiddleware.cs

@@ -137,11 +137,6 @@ namespace Jellyfin.Server.Middleware
 
         private string NormalizeExceptionMessage(string msg)
         {
-            if (msg == null)
-            {
-                return string.Empty;
-            }
-
             // Strip any information we don't want to reveal
             return msg.Replace(
                     _configuration.ApplicationPaths.ProgramSystemPath,

+ 1 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations
                 .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
                 .OfType<IMigrationRoutine>()
                 .ToArray();
-            var migrationOptions = ((IConfigurationManager)host.ConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+            var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
 
             if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
             {

+ 1 - 1
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs

@@ -92,7 +92,7 @@ namespace Jellyfin.Server.Migrations.Routines
                     if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid))
                     {
                         // This is not a valid Guid, see if it is an internal ID from an old Emby schema
-                        _logger.LogWarning("Invalid Guid in UserId column: ", entry[6].ToString());
+                        _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString());
 
                         using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
                         statement.TryBind("@Id", entry[6].ToString());

+ 0 - 1
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Text.Json;

+ 5 - 6
Jellyfin.Server/Program.cs

@@ -5,7 +5,6 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Reflection;
-using System.Runtime.InteropServices;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -121,11 +120,11 @@ namespace Jellyfin.Server
 
             // Log uncaught exceptions to the logging instead of std error
             AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
-            AppDomain.CurrentDomain.UnhandledException += (sender, e)
+            AppDomain.CurrentDomain.UnhandledException += (_, e)
                 => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
 
             // Intercept Ctrl+C and Ctrl+Break
-            Console.CancelKeyPress += (sender, e) =>
+            Console.CancelKeyPress += (_, e) =>
             {
                 if (_tokenSource.IsCancellationRequested)
                 {
@@ -139,7 +138,7 @@ namespace Jellyfin.Server
             };
 
             // Register a SIGTERM handler
-            AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
+            AppDomain.CurrentDomain.ProcessExit += (_, _) =>
             {
                 if (_tokenSource.IsCancellationRequested)
                 {
@@ -180,7 +179,7 @@ namespace Jellyfin.Server
                             "The server is expected to host the web client, but the provided content directory is either " +
                             $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
                             "server, you may set the '--nowebclient' command line flag, or set" +
-                            $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
+                            $"'{ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
                     }
                 }
 
@@ -543,7 +542,7 @@ namespace Jellyfin.Server
             // Get a stream of the resource contents
             // NOTE: The .csproj name is used instead of the assembly name in the resource path
             const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
-            await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+            await using Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
                 ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
 
             // Copy the resource contents to the expected file path for the config file

+ 1 - 1
MediaBrowser.Common/Extensions/ProcessExtensions.cs

@@ -40,7 +40,7 @@ namespace MediaBrowser.Common.Extensions
 
             // Add an event handler for the process exit event
             var tcs = new TaskCompletionSource<bool>();
-            process.Exited += (sender, args) => tcs.TrySetResult(true);
+            process.Exited += (_, _) => tcs.TrySetResult(true);
 
             // Return immediately if the process has already exited
             if (process.HasExitedSafe())

+ 24 - 24
MediaBrowser.Common/Net/IPHost.cs

@@ -4,7 +4,6 @@ using System.Linq;
 using System.Net;
 using System.Net.Sockets;
 using System.Text.RegularExpressions;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Common.Net
 {
@@ -196,7 +195,7 @@ namespace MediaBrowser.Common.Net
                 return res;
             }
 
-            throw new InvalidCastException("Host does not contain a valid value. {host}");
+            throw new InvalidCastException($"Host does not contain a valid value. {host}");
         }
 
         /// <summary>
@@ -221,7 +220,7 @@ namespace MediaBrowser.Common.Net
                 return res;
             }
 
-            throw new InvalidCastException("Host does not contain a valid value. {host}");
+            throw new InvalidCastException($"Host does not contain a valid value. {host}");
         }
 
         /// <summary>
@@ -349,7 +348,7 @@ namespace MediaBrowser.Common.Net
                     }
                 }
 
-                output = output[0..^1];
+                output = output[..^1];
 
                 if (moreThanOne)
                 {
@@ -400,7 +399,7 @@ namespace MediaBrowser.Common.Net
             if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout)))
             {
                 _lastResolved = DateTime.UtcNow;
-                ResolveHostInternal().GetAwaiter().GetResult();
+                ResolveHostInternal();
                 Resolved = true;
             }
 
@@ -410,30 +409,31 @@ namespace MediaBrowser.Common.Net
         /// <summary>
         /// Task that looks up a Host name and returns its IP addresses.
         /// </summary>
-        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
-        private async Task ResolveHostInternal()
+        private void ResolveHostInternal()
         {
-            if (!string.IsNullOrEmpty(HostName))
+            var hostName = HostName;
+            if (string.IsNullOrEmpty(hostName))
             {
-                // Resolves the host name - so save a DNS lookup.
-                if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase))
+                return;
+            }
+
+            // Resolves the host name - so save a DNS lookup.
+            if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase))
+            {
+                _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback };
+                return;
+            }
+
+            if (Uri.CheckHostName(hostName) == UriHostNameType.Dns)
+            {
+                try
                 {
-                    _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback };
-                    return;
+                    _addresses = Dns.GetHostEntry(hostName).AddressList;
                 }
-
-                if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns))
+                catch (SocketException ex)
                 {
-                    try
-                    {
-                        IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false);
-                        _addresses = ip.AddressList;
-                    }
-                    catch (SocketException ex)
-                    {
-                        // Log and then ignore socket errors, as the result value will just be an empty array.
-                        Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message);
-                    }
+                    // Log and then ignore socket errors, as the result value will just be an empty array.
+                    Debug.WriteLine("GetHostAddresses failed with {Message}.", ex.Message);
                 }
             }
         }

+ 2 - 2
MediaBrowser.Common/Plugins/BasePluginOfT.cs

@@ -47,10 +47,10 @@ namespace MediaBrowser.Common.Plugins
             var assemblyFilePath = assembly.Location;
 
             var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-            if (!Directory.Exists(dataFolderPath) && Version != null)
+            if (Version != null && !Directory.Exists(dataFolderPath))
             {
                 // Try again with the version number appended to the folder name.
-                dataFolderPath = dataFolderPath + "_" + Version.ToString();
+                dataFolderPath += "_" + Version.ToString();
             }
 
             SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);

+ 4 - 4
MediaBrowser.Common/Providers/ProviderIdParsers.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Common.Providers
         /// <param name="text">The text to parse.</param>
         /// <param name="imdbId">The parsed IMDb id.</param>
         /// <returns>True if parsing was successful, false otherwise.</returns>
-        public static bool TryFindImdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> imdbId)
+        public static bool TryFindImdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> imdbId)
         {
             // imdb id is at least 9 chars (tt + 7 numbers)
             while (text.Length >= 2 + ImdbMinNumbers)
@@ -62,7 +62,7 @@ namespace MediaBrowser.Common.Providers
         /// <param name="text">The text with the url to parse.</param>
         /// <param name="tmdbId">The parsed TMDb id.</param>
         /// <returns>True if parsing was successful, false otherwise.</returns>
-        public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+        public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
             => TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId);
 
         /// <summary>
@@ -71,7 +71,7 @@ namespace MediaBrowser.Common.Providers
         /// <param name="text">The text with the url to parse.</param>
         /// <param name="tmdbId">The parsed TMDb id.</param>
         /// <returns>True if parsing was successful, false otherwise.</returns>
-        public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+        public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
             => TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId);
 
         /// <summary>
@@ -80,7 +80,7 @@ namespace MediaBrowser.Common.Providers
         /// <param name="text">The text with the url to parse.</param>
         /// <param name="tvdbId">The parsed TVDb id.</param>
         /// <returns>True if parsing was successful, false otherwise.</returns>
-        public static bool TryFindTvdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tvdbId)
+        public static bool TryFindTvdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tvdbId)
             => TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId);
 
         private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId)

+ 4 - 4
MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs

@@ -1,6 +1,5 @@
-#nullable disable
-
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Extensions;
@@ -16,7 +15,7 @@ namespace MediaBrowser.Controller.BaseItemManager
     {
         private readonly IServerConfigurationManager _serverConfigurationManager;
 
-        private int _metadataRefreshConcurrency = 0;
+        private int _metadataRefreshConcurrency;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseItemManager"/> class.
@@ -101,7 +100,7 @@ namespace MediaBrowser.Controller.BaseItemManager
         /// Called when the configuration is updated.
         /// It will refresh the metadata throttler if the relevant config changed.
         /// </summary>
-        private void OnConfigurationUpdated(object sender, EventArgs e)
+        private void OnConfigurationUpdated(object? sender, EventArgs e)
         {
             int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency();
             if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency)
@@ -114,6 +113,7 @@ namespace MediaBrowser.Controller.BaseItemManager
         /// <summary>
         /// Creates the metadata refresh throttler.
         /// </summary>
+        [MemberNotNull(nameof(MetadataRefreshThrottler))]
         private void SetupMetadataThrottler()
         {
             MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency);

+ 1 - 3
MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System.Threading;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Configuration;
@@ -34,4 +32,4 @@ namespace MediaBrowser.Controller.BaseItemManager
         /// <returns><c>true</c> if image fetcher is enabled, else false.</returns>
         bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
     }
-}
+}

+ 4 - 5
MediaBrowser.Controller/Channels/ChannelItemResult.cs

@@ -1,7 +1,6 @@
-#nullable disable
-
-#pragma warning disable CA1002, CA2227, CS1591
+#pragma warning disable CS1591
 
+using System;
 using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Channels
@@ -10,10 +9,10 @@ namespace MediaBrowser.Controller.Channels
     {
         public ChannelItemResult()
         {
-            Items = new List<ChannelItemInfo>();
+            Items = Array.Empty<ChannelItemInfo>();
         }
 
-        public List<ChannelItemInfo> Items { get; set; }
+        public IReadOnlyList<ChannelItemInfo> Items { get; set; }
 
         public int? TotalRecordCount { get; set; }
     }

+ 1 - 1
MediaBrowser.Controller/Collections/CollectionCreationOptions.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CA2227, CS1591
+#pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;

+ 0 - 2
MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;

+ 3 - 5
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -16,17 +14,17 @@ namespace MediaBrowser.Controller.Collections
         /// <summary>
         /// Occurs when [collection created].
         /// </summary>
-        event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+        event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 
         /// <summary>
         /// Occurs when [items added to collection].
         /// </summary>
-        event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+        event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 
         /// <summary>
         /// Occurs when [items removed from collection].
         /// </summary>
-        event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+        event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 
         /// <summary>
         /// Creates the collection.

+ 0 - 2
MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Configuration;
 

+ 3 - 5
MediaBrowser.Controller/Dlna/IDlnaManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
@@ -22,7 +20,7 @@ namespace MediaBrowser.Controller.Dlna
         /// </summary>
         /// <param name="headers">The headers.</param>
         /// <returns>DeviceProfile.</returns>
-        DeviceProfile GetProfile(IHeaderDictionary headers);
+        DeviceProfile? GetProfile(IHeaderDictionary headers);
 
         /// <summary>
         /// Gets the default profile.
@@ -53,14 +51,14 @@ namespace MediaBrowser.Controller.Dlna
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <returns>DeviceProfile.</returns>
-        DeviceProfile GetProfile(string id);
+        DeviceProfile? GetProfile(string id);
 
         /// <summary>
         /// Gets the profile.
         /// </summary>
         /// <param name="deviceInfo">The device information.</param>
         /// <returns>DeviceProfile.</returns>
-        DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+        DeviceProfile? GetProfile(DeviceIdentification deviceInfo);
 
         /// <summary>
         /// Gets the server description XML.

+ 21 - 22
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
 
 using System;
 using System.Collections.Concurrent;
@@ -18,52 +18,51 @@ namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
     /// Specialized folder that can have items added to it's children by external entities.
-    /// Used for our RootFolder so plug-ins can add items.
+    /// Used for our RootFolder so plugins can add items.
     /// </summary>
     public class AggregateFolder : Folder
     {
+        private readonly object _childIdsLock = new object();
+
+        /// <summary>
+        /// The _virtual children.
+        /// </summary>
+        private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
         private bool _requiresRefresh;
+        private Guid[] _childrenIds = null;
 
         public AggregateFolder()
         {
             PhysicalLocationsList = Array.Empty<string>();
         }
 
-        [JsonIgnore]
-        public override bool IsPhysicalRoot => true;
-
-        public override bool CanDelete()
-        {
-            return false;
-        }
-
-        [JsonIgnore]
-        public override bool SupportsPlayedStatus => false;
-
-        /// <summary>
-        /// The _virtual children.
-        /// </summary>
-        private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
-
         /// <summary>
         /// Gets the virtual children.
         /// </summary>
         /// <value>The virtual children.</value>
         public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren;
 
+        [JsonIgnore]
+        public override bool IsPhysicalRoot => true;
+
+        [JsonIgnore]
+        public override bool SupportsPlayedStatus => false;
+
         [JsonIgnore]
         public override string[] PhysicalLocations => PhysicalLocationsList;
 
         public string[] PhysicalLocationsList { get; set; }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
         {
             return CreateResolveArgs(directoryService, true).FileSystemChildren;
         }
 
-        private Guid[] _childrenIds = null;
-        private readonly object _childIdsLock = new object();
-
         protected override List<BaseItem> LoadChildren()
         {
             lock (_childIdsLock)
@@ -169,7 +168,7 @@ namespace MediaBrowser.Controller.Entities
         /// Adds the virtual child.
         /// </summary>
         /// <param name="child">The child.</param>
-        /// <exception cref="ArgumentNullException"></exception>
+        /// <exception cref="ArgumentNullException">Throws if child is null.</exception>
         public void AddVirtualChild(BaseItem child)
         {
             if (child == null)

+ 17 - 17
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1724, CA1826, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -25,6 +25,12 @@ namespace MediaBrowser.Controller.Entities.Audio
         IHasLookupInfo<SongInfo>,
         IHasMediaSources
     {
+        public Audio()
+        {
+            Artists = Array.Empty<string>();
+            AlbumArtists = Array.Empty<string>();
+        }
+
         /// <inheritdoc />
         [JsonIgnore]
         public IReadOnlyList<string> Artists { get; set; }
@@ -33,17 +39,6 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public IReadOnlyList<string> AlbumArtists { get; set; }
 
-        public Audio()
-        {
-            Artists = Array.Empty<string>();
-            AlbumArtists = Array.Empty<string>();
-        }
-
-        public override double GetDefaultPrimaryImageAspectRatio()
-        {
-            return 1;
-        }
-
         [JsonIgnore]
         public override bool SupportsPlayedStatus => true;
 
@@ -62,11 +57,6 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public override Folder LatestItemsIndexContainer => AlbumEntity;
 
-        public override bool CanDownload()
-        {
-            return IsFileProtocol;
-        }
-
         [JsonIgnore]
         public MusicAlbum AlbumEntity => FindParent<MusicAlbum>();
 
@@ -77,6 +67,16 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public override string MediaType => Model.Entities.MediaType.Audio;
 
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            return 1;
+        }
+
+        public override bool CanDownload()
+        {
+            return IsFileProtocol;
+        }
+
         /// <summary>
         /// Creates the name of the sort.
         /// </summary>

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
 
 namespace MediaBrowser.Controller.Entities.Audio
 {

+ 26 - 26
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1721, CA1826, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -23,18 +23,18 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
     {
-        /// <inheritdoc />
-        public IReadOnlyList<string> AlbumArtists { get; set; }
-
-        /// <inheritdoc />
-        public IReadOnlyList<string> Artists { get; set; }
-
         public MusicAlbum()
         {
             Artists = Array.Empty<string>();
             AlbumArtists = Array.Empty<string>();
         }
 
+        /// <inheritdoc />
+        public IReadOnlyList<string> AlbumArtists { get; set; }
+
+        /// <inheritdoc />
+        public IReadOnlyList<string> Artists { get; set; }
+
         [JsonIgnore]
         public override bool SupportsAddingToPlaylist => true;
 
@@ -44,6 +44,25 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true));
 
+        [JsonIgnore]
+        public override bool SupportsPlayedStatus => false;
+
+        [JsonIgnore]
+        public override bool SupportsCumulativeRunTimeTicks => true;
+
+        [JsonIgnore]
+        public string AlbumArtist => AlbumArtists.FirstOrDefault();
+
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
+        /// <summary>
+        /// Gets the tracks.
+        /// </summary>
+        /// <value>The tracks.</value>
+        [JsonIgnore]
+        public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
+
         public MusicArtist GetMusicArtist(DtoOptions options)
         {
             var parents = GetParents();
@@ -64,25 +83,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return null;
         }
 
-        [JsonIgnore]
-        public override bool SupportsPlayedStatus => false;
-
-        [JsonIgnore]
-        public override bool SupportsCumulativeRunTimeTicks => true;
-
-        [JsonIgnore]
-        public string AlbumArtist => AlbumArtists.FirstOrDefault();
-
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
-        /// <summary>
-        /// Gets the tracks.
-        /// </summary>
-        /// <value>The tracks.</value>
-        [JsonIgnore]
-        public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
-
         protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
         {
             return Tracks;

+ 32 - 30
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -44,6 +44,36 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public override bool SupportsPlayedStatus => false;
 
+        /// <summary>
+        /// Gets the folder containing the item.
+        /// If the item is a folder, it returns the folder itself.
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [JsonIgnore]
+        public override string ContainingFolderPath => Path;
+
+        [JsonIgnore]
+        public override IEnumerable<BaseItem> Children
+        {
+            get
+            {
+                if (IsAccessedByName)
+                {
+                    return new List<BaseItem>();
+                }
+
+                return base.Children;
+            }
+        }
+
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             return 1;
@@ -65,20 +95,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return LibraryManager.GetItemList(query);
         }
 
-        [JsonIgnore]
-        public override IEnumerable<BaseItem> Children
-        {
-            get
-            {
-                if (IsAccessedByName)
-                {
-                    return new List<BaseItem>();
-                }
-
-                return base.Children;
-            }
-        }
-
         public override int GetChildCount(User user)
         {
             return IsAccessedByName ? 0 : base.GetChildCount(user);
@@ -113,14 +129,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return list;
         }
 
-        /// <summary>
-        /// Gets the folder containing the item.
-        /// If the item is a folder, it returns the folder itself.
-        /// </summary>
-        /// <value>The containing folder path.</value>
-        [JsonIgnore]
-        public override string ContainingFolderPath => Path;
-
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -167,14 +175,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return info;
         }
 
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
-        public static string GetPath(string name)
-        {
-            return GetPath(name, true);
-        }
-
         public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
@@ -208,6 +208,8 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
+        /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+        /// <returns>True if metadata changed.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {
             var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);

+ 18 - 16
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -15,19 +15,6 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicGenre : BaseItem, IItemByName
     {
-        public override List<string> GetUserDataKeys()
-        {
-            var list = base.GetUserDataKeys();
-
-            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
-            return list;
-        }
-
-        public override string CreatePresentationUniqueKey()
-        {
-            return GetUserDataKeys()[0];
-        }
-
         [JsonIgnore]
         public override bool SupportsAddingToPlaylist => true;
 
@@ -45,6 +32,22 @@ namespace MediaBrowser.Controller.Entities.Audio
         [JsonIgnore]
         public override string ContainingFolderPath => Path;
 
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+
+        public override string CreatePresentationUniqueKey()
+        {
+            return GetUserDataKeys()[0];
+        }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             return 1;
@@ -60,9 +63,6 @@ namespace MediaBrowser.Controller.Entities.Audio
             return true;
         }
 
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
         public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             query.GenreIds = new[] { Id };
@@ -106,6 +106,8 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
+        /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+        /// <returns>True if metadata changed.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {
             var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);

+ 1 - 1
MediaBrowser.Controller/Entities/AudioBook.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1724, CS1591
 
 using System;
 using System.Text.Json.Serialization;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 523 - 381
MediaBrowser.Controller/Entities/BaseItem.cs


+ 5 - 0
MediaBrowser.Controller/Entities/BaseItemExtensions.cs

@@ -64,6 +64,8 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <param name="source">The source object.</param>
         /// <param name="dest">The destination object.</param>
+        /// <typeparam name="T">Source type.</typeparam>
+        /// <typeparam name="TU">Destination type.</typeparam>
         public static void DeepCopy<T, TU>(this T source, TU dest)
             where T : BaseItem
             where TU : BaseItem
@@ -109,6 +111,9 @@ namespace MediaBrowser.Controller.Entities
         /// Copies all properties on newly created object. Skips properties that do not exist.
         /// </summary>
         /// <param name="source">The source object.</param>
+        /// <typeparam name="T">Source type.</typeparam>
+        /// <typeparam name="TU">Destination type.</typeparam>
+        /// <returns>Destination object.</returns>
         public static TU DeepCopy<T, TU>(this T source)
             where T : BaseItem
             where TU : BaseItem, new()

+ 6 - 6
MediaBrowser.Controller/Entities/BasePluginFolder.cs

@@ -15,6 +15,12 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public virtual string CollectionType => null;
 
+        [JsonIgnore]
+        public override bool SupportsInheritedParentImages => false;
+
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
         public override bool CanDelete()
         {
             return false;
@@ -24,11 +30,5 @@ namespace MediaBrowser.Controller.Entities
         {
             return true;
         }
-
-        [JsonIgnore]
-        public override bool SupportsInheritedParentImages => false;
-
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
     }
 }

+ 20 - 20
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -41,6 +41,23 @@ namespace MediaBrowser.Controller.Entities
             PhysicalFolderIds = Array.Empty<Guid>();
         }
 
+        /// <summary>
+        /// Gets the display preferences id.
+        /// </summary>
+        /// <remarks>
+        /// Allow different display preferences for each collection folder.
+        /// </remarks>
+        /// <value>The display prefs id.</value>
+        [JsonIgnore]
+        public override Guid DisplayPreferencesId => Id;
+
+        [JsonIgnore]
+        public override string[] PhysicalLocations => PhysicalLocationsList;
+
+        public string[] PhysicalLocationsList { get; set; }
+
+        public Guid[] PhysicalFolderIds { get; set; }
+
         public static IXmlSerializer XmlSerializer { get; set; }
 
         public static IServerApplicationHost ApplicationHost { get; set; }
@@ -63,6 +80,9 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public override IEnumerable<BaseItem> Children => GetActualChildren();
 
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
         public override bool CanDelete()
         {
             return false;
@@ -160,23 +180,6 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        /// <summary>
-        /// Gets the display preferences id.
-        /// </summary>
-        /// <remarks>
-        /// Allow different display preferences for each collection folder.
-        /// </remarks>
-        /// <value>The display prefs id.</value>
-        [JsonIgnore]
-        public override Guid DisplayPreferencesId => Id;
-
-        [JsonIgnore]
-        public override string[] PhysicalLocations => PhysicalLocationsList;
-
-        public string[] PhysicalLocationsList { get; set; }
-
-        public Guid[] PhysicalFolderIds { get; set; }
-
         public override bool IsSaveLocalMetadataEnabled()
         {
             return true;
@@ -373,8 +376,5 @@ namespace MediaBrowser.Controller.Entities
 
             return result;
         }
-
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
     }
 }

+ 2 - 0
MediaBrowser.Controller/Entities/Extensions.cs

@@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Adds the trailer URL.
         /// </summary>
+        /// <param name="item">Media item.</param>
+        /// <param name="url">Trailer URL.</param>
         public static void AddTrailerUrl(this BaseItem item, string url)
         {
             if (string.IsNullOrEmpty(url))

+ 6 - 5
MediaBrowser.Controller/Entities/Folder.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1721, CA1819, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -165,6 +165,8 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public static ICollectionManager CollectionManager { get; set; }
+
         public override bool CanDelete()
         {
             if (IsRoot)
@@ -258,6 +260,7 @@ namespace MediaBrowser.Controller.Entities
         /// Loads our children.  Validation will occur externally.
         /// We want this synchronous.
         /// </summary>
+        /// <returns>Returns children.</returns>
         protected virtual List<BaseItem> LoadChildren()
         {
             // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
@@ -642,6 +645,8 @@ namespace MediaBrowser.Controller.Entities
         /// Get the children of this folder from the actual file system.
         /// </summary>
         /// <returns>IEnumerable{BaseItem}.</returns>
+        /// <param name="directoryService">The directory service to use for operation.</param>
+        /// <returns>Returns set of base items.</returns>
         protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
         {
             var collectionType = LibraryManager.GetContentType(this);
@@ -998,8 +1003,6 @@ namespace MediaBrowser.Controller.Entities
             return PostFilterAndSort(items, query, true);
         }
 
-        public static ICollectionManager CollectionManager { get; set; }
-
         protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
         {
             var user = query.User;
@@ -1666,7 +1669,6 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="user">The user.</param>
         /// <param name="datePlayed">The date played.</param>
         /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
-        /// <returns>Task.</returns>
         public override void MarkPlayed(
             User user,
             DateTime? datePlayed,
@@ -1708,7 +1710,6 @@ namespace MediaBrowser.Controller.Entities
         /// Marks the unplayed.
         /// </summary>
         /// <param name="user">The user.</param>
-        /// <returns>Task.</returns>
         public override void MarkUnplayed(User user)
         {
             var itemsResult = GetItemList(new InternalItemsQuery

+ 1 - 1
MediaBrowser.Controller/Entities/ICollectionFolder.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
 
 using System;
 

+ 2 - 0
MediaBrowser.Controller/Entities/IHasMediaSources.cs

@@ -20,6 +20,8 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Gets the media sources.
         /// </summary>
+        /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param>
+        /// <returns>A list of media sources.</returns>
         List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
 
         List<MediaStream> GetMediaStreams();

+ 1 - 1
MediaBrowser.Controller/Entities/IHasShares.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
 
 namespace MediaBrowser.Controller.Entities
 {

+ 3 - 0
MediaBrowser.Controller/Entities/IHasTrailers.cs

@@ -39,6 +39,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Gets the trailer count.
         /// </summary>
+        /// <param name="item">Media item.</param>
         /// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
         public static int GetTrailerCount(this IHasTrailers item)
             => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count;
@@ -46,6 +47,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Gets the trailer ids.
         /// </summary>
+        /// <param name="item">Media item.</param>
         /// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
         public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item)
         {
@@ -70,6 +72,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Gets the trailers.
         /// </summary>
+        /// <param name="item">Media item.</param>
         /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns>
         public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item)
         {

+ 79 - 79
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1044, CA1819, CA2227, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -12,6 +12,55 @@ namespace MediaBrowser.Controller.Entities
 {
     public class InternalItemsQuery
     {
+        public InternalItemsQuery()
+        {
+            AlbumArtistIds = Array.Empty<Guid>();
+            AlbumIds = Array.Empty<Guid>();
+            AncestorIds = Array.Empty<Guid>();
+            ArtistIds = Array.Empty<Guid>();
+            BlockUnratedItems = Array.Empty<UnratedItem>();
+            BoxSetLibraryFolders = Array.Empty<Guid>();
+            ChannelIds = Array.Empty<Guid>();
+            ContributingArtistIds = Array.Empty<Guid>();
+            DtoOptions = new DtoOptions();
+            EnableTotalRecordCount = true;
+            ExcludeArtistIds = Array.Empty<Guid>();
+            ExcludeInheritedTags = Array.Empty<string>();
+            ExcludeItemIds = Array.Empty<Guid>();
+            ExcludeItemTypes = Array.Empty<string>();
+            ExcludeTags = Array.Empty<string>();
+            GenreIds = Array.Empty<Guid>();
+            Genres = Array.Empty<string>();
+            GroupByPresentationUniqueKey = true;
+            ImageTypes = Array.Empty<ImageType>();
+            IncludeItemTypes = Array.Empty<string>();
+            ItemIds = Array.Empty<Guid>();
+            MediaTypes = Array.Empty<string>();
+            MinSimilarityScore = 20;
+            OfficialRatings = Array.Empty<string>();
+            OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
+            PersonIds = Array.Empty<Guid>();
+            PersonTypes = Array.Empty<string>();
+            PresetViews = Array.Empty<string>();
+            SeriesStatuses = Array.Empty<SeriesStatus>();
+            SourceTypes = Array.Empty<SourceType>();
+            StudioIds = Array.Empty<Guid>();
+            Tags = Array.Empty<string>();
+            TopParentIds = Array.Empty<Guid>();
+            TrailerTypes = Array.Empty<TrailerType>();
+            VideoTypes = Array.Empty<VideoType>();
+            Years = Array.Empty<int>();
+        }
+
+        public InternalItemsQuery(User? user)
+            : this()
+        {
+            if (user != null)
+            {
+                SetUser(user);
+            }
+        }
+
         public bool Recursive { get; set; }
 
         public int? StartIndex { get; set; }
@@ -186,23 +235,6 @@ namespace MediaBrowser.Controller.Entities
 
         public Guid[] TopParentIds { get; set; }
 
-        public BaseItem? Parent
-        {
-            set
-            {
-                if (value == null)
-                {
-                    ParentId = Guid.Empty;
-                    ParentType = null;
-                }
-                else
-                {
-                    ParentId = value.Id;
-                    ParentType = value.GetType().Name;
-                }
-            }
-        }
-
         public string[] PresetViews { get; set; }
 
         public TrailerType[] TrailerTypes { get; set; }
@@ -270,70 +302,21 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public bool? DisplayAlbumFolders { get; set; }
 
-        public InternalItemsQuery()
-        {
-            AlbumArtistIds = Array.Empty<Guid>();
-            AlbumIds = Array.Empty<Guid>();
-            AncestorIds = Array.Empty<Guid>();
-            ArtistIds = Array.Empty<Guid>();
-            BlockUnratedItems = Array.Empty<UnratedItem>();
-            BoxSetLibraryFolders = Array.Empty<Guid>();
-            ChannelIds = Array.Empty<Guid>();
-            ContributingArtistIds = Array.Empty<Guid>();
-            DtoOptions = new DtoOptions();
-            EnableTotalRecordCount = true;
-            ExcludeArtistIds = Array.Empty<Guid>();
-            ExcludeInheritedTags = Array.Empty<string>();
-            ExcludeItemIds = Array.Empty<Guid>();
-            ExcludeItemTypes = Array.Empty<string>();
-            ExcludeTags = Array.Empty<string>();
-            GenreIds = Array.Empty<Guid>();
-            Genres = Array.Empty<string>();
-            GroupByPresentationUniqueKey = true;
-            ImageTypes = Array.Empty<ImageType>();
-            IncludeItemTypes = Array.Empty<string>();
-            ItemIds = Array.Empty<Guid>();
-            MediaTypes = Array.Empty<string>();
-            MinSimilarityScore = 20;
-            OfficialRatings = Array.Empty<string>();
-            OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
-            PersonIds = Array.Empty<Guid>();
-            PersonTypes = Array.Empty<string>();
-            PresetViews = Array.Empty<string>();
-            SeriesStatuses = Array.Empty<SeriesStatus>();
-            SourceTypes = Array.Empty<SourceType>();
-            StudioIds = Array.Empty<Guid>();
-            Tags = Array.Empty<string>();
-            TopParentIds = Array.Empty<Guid>();
-            TrailerTypes = Array.Empty<TrailerType>();
-            VideoTypes = Array.Empty<VideoType>();
-            Years = Array.Empty<int>();
-        }
-
-        public InternalItemsQuery(User? user)
-            : this()
-        {
-            if (user != null)
-            {
-                SetUser(user);
-            }
-        }
-
-        public void SetUser(User user)
+        public BaseItem? Parent
         {
-            MaxParentalRating = user.MaxParentalAgeRating;
-
-            if (MaxParentalRating.HasValue)
+            set
             {
-                string other = UnratedItem.Other.ToString();
-                BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
-                    .Where(i => i != other)
-                    .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+                if (value == null)
+                {
+                    ParentId = Guid.Empty;
+                    ParentType = null;
+                }
+                else
+                {
+                    ParentId = value.Id;
+                    ParentType = value.GetType().Name;
+                }
             }
-
-            ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
-
-            User = user;
         }
 
         public Dictionary<string, string>? HasAnyProviderId { get; set; }
@@ -361,5 +344,22 @@ namespace MediaBrowser.Controller.Entities
         public string? SearchTerm { get; set; }
 
         public string? SeriesTimerId { get; set; }
+
+        public void SetUser(User user)
+        {
+            MaxParentalRating = user.MaxParentalAgeRating;
+
+            if (MaxParentalRating.HasValue)
+            {
+                string other = UnratedItem.Other.ToString();
+                BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+                    .Where(i => i != other)
+                    .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+            }
+
+            ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+
+            User = user;
+        }
     }
 }

+ 25 - 25
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1721, CA1819, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -49,6 +49,30 @@ namespace MediaBrowser.Controller.Entities.Movies
         /// <value>The display order.</value>
         public string DisplayOrder { get; set; }
 
+        [JsonIgnore]
+        private bool IsLegacyBoxSet
+        {
+            get
+            {
+                if (string.IsNullOrEmpty(Path))
+                {
+                    return false;
+                }
+
+                if (LinkedChildren.Length > 0)
+                {
+                    return false;
+                }
+
+                return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
+            }
+        }
+
+        [JsonIgnore]
+        public override bool IsPreSorted => true;
+
+        public Guid[] LibraryFolderIds { get; set; }
+
         protected override bool GetBlockUnratedValue(User user)
         {
             return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
@@ -83,28 +107,6 @@ namespace MediaBrowser.Controller.Entities.Movies
             return new List<BaseItem>();
         }
 
-        [JsonIgnore]
-        private bool IsLegacyBoxSet
-        {
-            get
-            {
-                if (string.IsNullOrEmpty(Path))
-                {
-                    return false;
-                }
-
-                if (LinkedChildren.Length > 0)
-                {
-                    return false;
-                }
-
-                return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
-            }
-        }
-
-        [JsonIgnore]
-        public override bool IsPreSorted => true;
-
         public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
         {
             return true;
@@ -191,8 +193,6 @@ namespace MediaBrowser.Controller.Entities.Movies
             return IsVisible(user);
         }
 
-        public Guid[] LibraryFolderIds { get; set; }
-
         private Guid[] GetLibraryFolderIds(User user)
         {
             return LibraryManager.GetUserRootFolder().GetChildren(user, true)

+ 22 - 20
MediaBrowser.Controller/Entities/Person.cs

@@ -16,6 +16,26 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
     {
+        /// <summary>
+        /// Gets the folder containing the item.
+        /// If the item is a folder, it returns the folder itself.
+        /// </summary>
+        /// <value>The containing folder path.</value>
+        [JsonIgnore]
+        public override string ContainingFolderPath => Path;
+
+        /// <summary>
+        /// Gets a value indicating whether to enable alpha numeric sorting.
+        /// </summary>
+        [JsonIgnore]
+        public override bool EnableAlphaNumericSorting => false;
+
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
+        [JsonIgnore]
+        public override bool SupportsAncestors => false;
+
         public override List<string> GetUserDataKeys()
         {
             var list = base.GetUserDataKeys();
@@ -49,14 +69,6 @@ namespace MediaBrowser.Controller.Entities
             return LibraryManager.GetItemList(query);
         }
 
-        /// <summary>
-        /// Gets the folder containing the item.
-        /// If the item is a folder, it returns the folder itself.
-        /// </summary>
-        /// <value>The containing folder path.</value>
-        [JsonIgnore]
-        public override string ContainingFolderPath => Path;
-
         public override bool CanDelete()
         {
             return false;
@@ -67,18 +79,6 @@ namespace MediaBrowser.Controller.Entities
             return true;
         }
 
-        /// <summary>
-        /// Gets a value indicating whether to enable alpha numeric sorting.
-        /// </summary>
-        [JsonIgnore]
-        public override bool EnableAlphaNumericSorting => false;
-
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
-        [JsonIgnore]
-        public override bool SupportsAncestors => false;
-
         public static string GetPath(string name)
         {
             return GetPath(name, true);
@@ -129,6 +129,8 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
+        /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+        /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {
             var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);

+ 1 - 1
MediaBrowser.Controller/Entities/PersonInfo.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
 
 using System;
 using System.Collections.Generic;

+ 24 - 24
MediaBrowser.Controller/Entities/Photo.cs

@@ -36,6 +36,30 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public string CameraMake { get; set; }
+
+        public string CameraModel { get; set; }
+
+        public string Software { get; set; }
+
+        public double? ExposureTime { get; set; }
+
+        public double? FocalLength { get; set; }
+
+        public ImageOrientation? Orientation { get; set; }
+
+        public double? Aperture { get; set; }
+
+        public double? ShutterSpeed { get; set; }
+
+        public double? Latitude { get; set; }
+
+        public double? Longitude { get; set; }
+
+        public double? Altitude { get; set; }
+
+        public int? IsoSpeedRating { get; set; }
+
         public override bool CanDownload()
         {
             return true;
@@ -69,29 +93,5 @@ namespace MediaBrowser.Controller.Entities
 
             return base.GetDefaultPrimaryImageAspectRatio();
         }
-
-        public string CameraMake { get; set; }
-
-        public string CameraModel { get; set; }
-
-        public string Software { get; set; }
-
-        public double? ExposureTime { get; set; }
-
-        public double? FocalLength { get; set; }
-
-        public ImageOrientation? Orientation { get; set; }
-
-        public double? Aperture { get; set; }
-
-        public double? ShutterSpeed { get; set; }
-
-        public double? Latitude { get; set; }
-
-        public double? Longitude { get; set; }
-
-        public double? Altitude { get; set; }
-
-        public int? IsoSpeedRating { get; set; }
     }
 }

+ 18 - 16
MediaBrowser.Controller/Entities/Studio.cs

@@ -15,19 +15,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Studio : BaseItem, IItemByName
     {
-        public override List<string> GetUserDataKeys()
-        {
-            var list = base.GetUserDataKeys();
-
-            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
-            return list;
-        }
-
-        public override string CreatePresentationUniqueKey()
-        {
-            return GetUserDataKeys()[0];
-        }
-
         /// <summary>
         /// Gets the folder containing the item.
         /// If the item is a folder, it returns the folder itself.
@@ -42,6 +29,22 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public override bool SupportsAncestors => false;
 
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+            return list;
+        }
+
+        public override string CreatePresentationUniqueKey()
+        {
+            return GetUserDataKeys()[0];
+        }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             double value = 16;
@@ -67,9 +70,6 @@ namespace MediaBrowser.Controller.Entities
             return LibraryManager.GetItemList(query);
         }
 
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
         public static string GetPath(string name)
         {
             return GetPath(name, true);
@@ -105,6 +105,8 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
+        /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+        /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {
             var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);

+ 68 - 68
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -49,12 +49,6 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <value>The index number.</value>
         public int? IndexNumberEnd { get; set; }
 
-        public string FindSeriesSortName()
-        {
-            var series = Series;
-            return series == null ? SeriesName : series.SortName;
-        }
-
         [JsonIgnore]
         protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1;
 
@@ -76,45 +70,6 @@ namespace MediaBrowser.Controller.Entities.TV
         [JsonIgnore]
         protected override bool EnableDefaultVideoUserDataKeys => false;
 
-        public override double GetDefaultPrimaryImageAspectRatio()
-        {
-            // hack for tv plugins
-            if (SourceType == SourceType.Channel)
-            {
-                return 0;
-            }
-
-            return 16.0 / 9;
-        }
-
-        public override List<string> GetUserDataKeys()
-        {
-            var list = base.GetUserDataKeys();
-
-            var series = Series;
-            if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
-            {
-                var seriesUserDataKeys = series.GetUserDataKeys();
-                var take = seriesUserDataKeys.Count;
-                if (seriesUserDataKeys.Count > 1)
-                {
-                    take--;
-                }
-
-                var newList = seriesUserDataKeys.GetRange(0, take);
-                var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
-                for (int i = 0; i < take; i++)
-                {
-                    newList[i] = newList[i] + suffix;
-                }
-
-                newList.AddRange(list);
-                list = newList;
-            }
-
-            return list;
-        }
-
         /// <summary>
         /// Gets the Episode's Series Instance.
         /// </summary>
@@ -161,6 +116,74 @@ namespace MediaBrowser.Controller.Entities.TV
         [JsonIgnore]
         public string SeasonName { get; set; }
 
+        [JsonIgnore]
+        public override bool SupportsRemoteImageDownloading
+        {
+            get
+            {
+                if (IsMissingEpisode)
+                {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+
+        [JsonIgnore]
+        public bool IsMissingEpisode => LocationType == LocationType.Virtual;
+
+        [JsonIgnore]
+        public Guid SeasonId { get; set; }
+
+        [JsonIgnore]
+        public Guid SeriesId { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            var series = Series;
+            return series == null ? SeriesName : series.SortName;
+        }
+
+        public override double GetDefaultPrimaryImageAspectRatio()
+        {
+            // hack for tv plugins
+            if (SourceType == SourceType.Channel)
+            {
+                return 0;
+            }
+
+            return 16.0 / 9;
+        }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            var series = Series;
+            if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
+            {
+                var seriesUserDataKeys = series.GetUserDataKeys();
+                var take = seriesUserDataKeys.Count;
+                if (seriesUserDataKeys.Count > 1)
+                {
+                    take--;
+                }
+
+                var newList = seriesUserDataKeys.GetRange(0, take);
+                var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
+                for (int i = 0; i < take; i++)
+                {
+                    newList[i] = newList[i] + suffix;
+                }
+
+                newList.AddRange(list);
+                list = newList;
+            }
+
+            return list;
+        }
+
         public string FindSeriesPresentationUniqueKey()
         {
             var series = Series;
@@ -242,29 +265,6 @@ namespace MediaBrowser.Controller.Entities.TV
             return false;
         }
 
-        [JsonIgnore]
-        public override bool SupportsRemoteImageDownloading
-        {
-            get
-            {
-                if (IsMissingEpisode)
-                {
-                    return false;
-                }
-
-                return true;
-            }
-        }
-
-        [JsonIgnore]
-        public bool IsMissingEpisode => LocationType == LocationType.Virtual;
-
-        [JsonIgnore]
-        public Guid SeasonId { get; set; }
-
-        [JsonIgnore]
-        public Guid SeriesId { get; set; }
-
         public Guid FindSeriesId()
         {
             var series = FindParent<Series>();

+ 48 - 44
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -38,6 +38,50 @@ namespace MediaBrowser.Controller.Entities.TV
         [JsonIgnore]
         public override Guid DisplayParentId => SeriesId;
 
+        /// <summary>
+        /// Gets this Episode's Series Instance.
+        /// </summary>
+        /// <value>The series.</value>
+        [JsonIgnore]
+        public Series Series
+        {
+            get
+            {
+                var seriesId = SeriesId;
+                if (seriesId == Guid.Empty)
+                {
+                    seriesId = FindSeriesId();
+                }
+
+                return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series);
+            }
+        }
+
+        [JsonIgnore]
+        public string SeriesPath
+        {
+            get
+            {
+                var series = Series;
+
+                if (series != null)
+                {
+                    return series.Path;
+                }
+
+                return System.IO.Path.GetDirectoryName(Path);
+            }
+        }
+
+        [JsonIgnore]
+        public string SeriesPresentationUniqueKey { get; set; }
+
+        [JsonIgnore]
+        public string SeriesName { get; set; }
+
+        [JsonIgnore]
+        public Guid SeriesId { get; set; }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             double value = 2;
@@ -80,41 +124,6 @@ namespace MediaBrowser.Controller.Entities.TV
             return result;
         }
 
-        /// <summary>
-        /// Gets this Episode's Series Instance.
-        /// </summary>
-        /// <value>The series.</value>
-        [JsonIgnore]
-        public Series Series
-        {
-            get
-            {
-                var seriesId = SeriesId;
-                if (seriesId == Guid.Empty)
-                {
-                    seriesId = FindSeriesId();
-                }
-
-                return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series);
-            }
-        }
-
-        [JsonIgnore]
-        public string SeriesPath
-        {
-            get
-            {
-                var series = Series;
-
-                if (series != null)
-                {
-                    return series.Path;
-                }
-
-                return System.IO.Path.GetDirectoryName(Path);
-            }
-        }
-
         public override string CreatePresentationUniqueKey()
         {
             if (IndexNumber.HasValue)
@@ -157,6 +166,9 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <summary>
         /// Gets the episodes.
         /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="options">The options to use.</param>
+        /// <returns>Set of episodes.</returns>
         public List<BaseItem> GetEpisodes(User user, DtoOptions options)
         {
             return GetEpisodes(Series, user, options);
@@ -193,15 +205,6 @@ namespace MediaBrowser.Controller.Entities.TV
             return UnratedItem.Series;
         }
 
-        [JsonIgnore]
-        public string SeriesPresentationUniqueKey { get; set; }
-
-        [JsonIgnore]
-        public string SeriesName { get; set; }
-
-        [JsonIgnore]
-        public Guid SeriesId { get; set; }
-
         public string FindSeriesPresentationUniqueKey()
         {
             var series = Series;
@@ -241,6 +244,7 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
+        /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {

+ 11 - 3
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -72,6 +72,9 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <value>The status.</value>
         public SeriesStatus? Status { get; set; }
 
+        [JsonIgnore]
+        public override bool StopRefreshIfLocalMetadataFound => false;
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             double value = 2;
@@ -394,6 +397,10 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <summary>
         /// Filters the episodes by season.
         /// </summary>
+        /// <param name="episodes">The episodes.</param>
+        /// <param name="parentSeason">The season.</param>
+        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+        /// <returns>The set of episodes.</returns>
         public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
         {
             var seasonNumber = parentSeason.IndexNumber;
@@ -424,6 +431,10 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <summary>
         /// Filters the episodes by season.
         /// </summary>
+        /// <param name="episodes">The episodes.</param>
+        /// <param name="seasonNumber">The season.</param>
+        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+        /// <returns>The set of episodes.</returns>
         public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
         {
             if (!includeSpecials || seasonNumber < 1)
@@ -499,8 +510,5 @@ namespace MediaBrowser.Controller.Entities.TV
 
             return list;
         }
-
-        [JsonIgnore]
-        public override bool StopRefreshIfLocalMetadataFound => false;
     }
 }

+ 4 - 4
MediaBrowser.Controller/Entities/Trailer.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
 
 using System;
 using System.Collections.Generic;
@@ -23,6 +23,9 @@ namespace MediaBrowser.Controller.Entities
             TrailerTypes = Array.Empty<TrailerType>();
         }
 
+        [JsonIgnore]
+        public override bool StopRefreshIfLocalMetadataFound => false;
+
         public TrailerType[] TrailerTypes { get; set; }
 
         public override double GetDefaultPrimaryImageAspectRatio()
@@ -97,8 +100,5 @@ namespace MediaBrowser.Controller.Entities
 
             return list;
         }
-
-        [JsonIgnore]
-        public override bool StopRefreshIfLocalMetadataFound => false;
     }
 }

+ 7 - 7
MediaBrowser.Controller/Entities/UserItemData.cs

@@ -12,6 +12,13 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class UserItemData
     {
+        public const double MinLikeValue = 6.5;
+
+        /// <summary>
+        /// The _rating.
+        /// </summary>
+        private double? _rating;
+
         /// <summary>
         /// Gets or sets the user id.
         /// </summary>
@@ -24,11 +31,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The key.</value>
         public string Key { get; set; }
 
-        /// <summary>
-        /// The _rating.
-        /// </summary>
-        private double? _rating;
-
         /// <summary>
         /// Gets or sets the users 0-10 rating.
         /// </summary>
@@ -93,8 +95,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The index of the subtitle stream.</value>
         public int? SubtitleStreamIndex { get; set; }
 
-        public const double MinLikeValue = 6.5;
-
         /// <summary>
         /// Gets or sets a value indicating whether the item is liked or not.
         /// This should never be serialized.

+ 21 - 21
MediaBrowser.Controller/Entities/UserRootFolder.cs

@@ -21,8 +21,28 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class UserRootFolder : Folder
     {
-        private List<Guid> _childrenIds = null;
         private readonly object _childIdsLock = new object();
+        private List<Guid> _childrenIds = null;
+
+        [JsonIgnore]
+        public override bool SupportsInheritedParentImages => false;
+
+        [JsonIgnore]
+        public override bool SupportsPlayedStatus => false;
+
+        [JsonIgnore]
+        protected override bool SupportsShortcutChildren => true;
+
+        [JsonIgnore]
+        public override bool IsPreSorted => true;
+
+        private void ClearCache()
+        {
+            lock (_childIdsLock)
+            {
+                _childrenIds = null;
+            }
+        }
 
         protected override List<BaseItem> LoadChildren()
         {
@@ -39,20 +59,6 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        [JsonIgnore]
-        public override bool SupportsInheritedParentImages => false;
-
-        [JsonIgnore]
-        public override bool SupportsPlayedStatus => false;
-
-        private void ClearCache()
-        {
-            lock (_childIdsLock)
-            {
-                _childrenIds = null;
-            }
-        }
-
         protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
         {
             if (query.Recursive)
@@ -74,12 +80,6 @@ namespace MediaBrowser.Controller.Entities
             return GetChildren(user, true).Count;
         }
 
-        [JsonIgnore]
-        protected override bool SupportsShortcutChildren => true;
-
-        [JsonIgnore]
-        public override bool IsPreSorted => true;
-
         protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
         {
             var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList();

+ 119 - 119
MediaBrowser.Controller/Entities/Video.cs

@@ -28,6 +28,14 @@ namespace MediaBrowser.Controller.Entities
         ISupportsPlaceHolders,
         IHasMediaSources
     {
+        public Video()
+        {
+            AdditionalParts = Array.Empty<string>();
+            LocalAlternateVersions = Array.Empty<string>();
+            SubtitleFiles = Array.Empty<string>();
+            LinkedAlternateVersions = Array.Empty<LinkedChild>();
+        }
+
         [JsonIgnore]
         public string PrimaryVersionId { get; set; }
 
@@ -74,30 +82,6 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public void SetPrimaryVersionId(string id)
-        {
-            if (string.IsNullOrEmpty(id))
-            {
-                PrimaryVersionId = null;
-            }
-            else
-            {
-                PrimaryVersionId = id;
-            }
-
-            PresentationUniqueKey = CreatePresentationUniqueKey();
-        }
-
-        public override string CreatePresentationUniqueKey()
-        {
-            if (!string.IsNullOrEmpty(PrimaryVersionId))
-            {
-                return PrimaryVersionId;
-            }
-
-            return base.CreatePresentationUniqueKey();
-        }
-
         [JsonIgnore]
         public override bool SupportsThemeMedia => true;
 
@@ -151,24 +135,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The aspect ratio.</value>
         public string AspectRatio { get; set; }
 
-        public Video()
-        {
-            AdditionalParts = Array.Empty<string>();
-            LocalAlternateVersions = Array.Empty<string>();
-            SubtitleFiles = Array.Empty<string>();
-            LinkedAlternateVersions = Array.Empty<LinkedChild>();
-        }
-
-        public override bool CanDownload()
-        {
-            if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
-            {
-                return false;
-            }
-
-            return IsFileProtocol;
-        }
-
         [JsonIgnore]
         public override bool SupportsAddingToPlaylist => true;
 
@@ -196,16 +162,6 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
 
-        public IEnumerable<Guid> GetAdditionalPartIds()
-        {
-            return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
-        }
-
-        public IEnumerable<Guid> GetLocalAlternateVersionIds()
-        {
-            return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
-        }
-
         public static ILiveTvManager LiveTvManager { get; set; }
 
         [JsonIgnore]
@@ -222,37 +178,77 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        protected override bool IsActiveRecording()
+        [JsonIgnore]
+        public bool IsCompleteMedia
         {
-            return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+            get
+            {
+                if (SourceType == SourceType.Channel)
+                {
+                    return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+                }
+
+                return !IsActiveRecording();
+            }
         }
 
-        public override bool CanDelete()
+        [JsonIgnore]
+        protected virtual bool EnableDefaultVideoUserDataKeys => true;
+
+        [JsonIgnore]
+        public override string ContainingFolderPath
         {
-            if (IsActiveRecording())
+            get
             {
-                return false;
-            }
+                if (IsStacked)
+                {
+                    return System.IO.Path.GetDirectoryName(Path);
+                }
 
-            return base.CanDelete();
+                if (!IsPlaceHolder)
+                {
+                    if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+                    {
+                        return Path;
+                    }
+                }
+
+                return base.ContainingFolderPath;
+            }
         }
 
         [JsonIgnore]
-        public bool IsCompleteMedia
+        public override string FileNameWithoutExtension
         {
             get
             {
-                if (SourceType == SourceType.Channel)
+                if (IsFileProtocol)
                 {
-                    return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+                    if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+                    {
+                        return System.IO.Path.GetFileName(Path);
+                    }
+
+                    return System.IO.Path.GetFileNameWithoutExtension(Path);
                 }
 
-                return !IsActiveRecording();
+                return null;
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [is3 D].
+        /// </summary>
+        /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
         [JsonIgnore]
-        protected virtual bool EnableDefaultVideoUserDataKeys => true;
+        public bool Is3D => Video3DFormat.HasValue;
+
+        /// <summary>
+        /// Gets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        [JsonIgnore]
+        public override string MediaType => Model.Entities.MediaType.Video;
 
         public override List<string> GetUserDataKeys()
         {
@@ -293,6 +289,65 @@ namespace MediaBrowser.Controller.Entities
             return list;
         }
 
+        public void SetPrimaryVersionId(string id)
+        {
+            if (string.IsNullOrEmpty(id))
+            {
+                PrimaryVersionId = null;
+            }
+            else
+            {
+                PrimaryVersionId = id;
+            }
+
+            PresentationUniqueKey = CreatePresentationUniqueKey();
+        }
+
+        public override string CreatePresentationUniqueKey()
+        {
+            if (!string.IsNullOrEmpty(PrimaryVersionId))
+            {
+                return PrimaryVersionId;
+            }
+
+            return base.CreatePresentationUniqueKey();
+        }
+
+        public override bool CanDownload()
+        {
+            if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
+            {
+                return false;
+            }
+
+            return IsFileProtocol;
+        }
+
+        protected override bool IsActiveRecording()
+        {
+            return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+        }
+
+        public override bool CanDelete()
+        {
+            if (IsActiveRecording())
+            {
+                return false;
+            }
+
+            return base.CanDelete();
+        }
+
+        public IEnumerable<Guid> GetAdditionalPartIds()
+        {
+            return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+        }
+
+        public IEnumerable<Guid> GetLocalAlternateVersionIds()
+        {
+            return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+        }
+
         private string GetUserDataKey(string providerId)
         {
             var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
@@ -328,47 +383,6 @@ namespace MediaBrowser.Controller.Entities
                 .OrderBy(i => i.SortName);
         }
 
-        [JsonIgnore]
-        public override string ContainingFolderPath
-        {
-            get
-            {
-                if (IsStacked)
-                {
-                    return System.IO.Path.GetDirectoryName(Path);
-                }
-
-                if (!IsPlaceHolder)
-                {
-                    if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
-                    {
-                        return Path;
-                    }
-                }
-
-                return base.ContainingFolderPath;
-            }
-        }
-
-        [JsonIgnore]
-        public override string FileNameWithoutExtension
-        {
-            get
-            {
-                if (IsFileProtocol)
-                {
-                    if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
-                    {
-                        return System.IO.Path.GetFileName(Path);
-                    }
-
-                    return System.IO.Path.GetFileNameWithoutExtension(Path);
-                }
-
-                return null;
-            }
-        }
-
         internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
         {
             var updateType = base.UpdateFromResolvedItem(newItem);
@@ -397,20 +411,6 @@ namespace MediaBrowser.Controller.Entities
             return updateType;
         }
 
-        /// <summary>
-        /// Gets a value indicating whether [is3 D].
-        /// </summary>
-        /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
-        [JsonIgnore]
-        public bool Is3D => Video3DFormat.HasValue;
-
-        /// <summary>
-        /// Gets the type of the media.
-        /// </summary>
-        /// <value>The type of the media.</value>
-        [JsonIgnore]
-        public override string MediaType => Model.Entities.MediaType.Video;
-
         protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
         {
             var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);

+ 17 - 17
MediaBrowser.Controller/Entities/Year.cs

@@ -15,13 +15,11 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Year : BaseItem, IItemByName
     {
-        public override List<string> GetUserDataKeys()
-        {
-            var list = base.GetUserDataKeys();
+        [JsonIgnore]
+        public override bool SupportsAncestors => false;
 
-            list.Insert(0, "Year-" + Name);
-            return list;
-        }
+        [JsonIgnore]
+        public override bool SupportsPeople => false;
 
         /// <summary>
         /// Gets the folder containing the item.
@@ -31,6 +29,19 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public override string ContainingFolderPath => Path;
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        public override List<string> GetUserDataKeys()
+        {
+            var list = base.GetUserDataKeys();
+
+            list.Insert(0, "Year-" + Name);
+            return list;
+        }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             double value = 2;
@@ -39,14 +50,6 @@ namespace MediaBrowser.Controller.Entities
             return value;
         }
 
-        [JsonIgnore]
-        public override bool SupportsAncestors => false;
-
-        public override bool CanDelete()
-        {
-            return false;
-        }
-
         public override bool IsSaveLocalMetadataEnabled()
         {
             return true;
@@ -76,9 +79,6 @@ namespace MediaBrowser.Controller.Entities
             return null;
         }
 
-        [JsonIgnore]
-        public override bool SupportsPeople => false;
-
         public static string GetPath(string name)
         {
             return GetPath(name, true);

+ 9 - 9
MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs

@@ -10,6 +10,15 @@ namespace MediaBrowser.Controller.MediaEncoding
 {
     public class BaseEncodingJobOptions
     {
+        public BaseEncodingJobOptions()
+        {
+            EnableAutoStreamCopy = true;
+            AllowVideoStreamCopy = true;
+            AllowAudioStreamCopy = true;
+            Context = EncodingContext.Streaming;
+            StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -191,14 +200,5 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             return null;
         }
-
-        public BaseEncodingJobOptions()
-        {
-            EnableAutoStreamCopy = true;
-            AllowVideoStreamCopy = true;
-            AllowAudioStreamCopy = true;
-            Context = EncodingContext.Streaming;
-            StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
     }
 }

+ 49 - 4
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -7,7 +7,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Runtime.InteropServices;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
@@ -16,9 +15,7 @@ using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Configuration;
 
 namespace MediaBrowser.Controller.MediaEncoding
 {
@@ -161,6 +158,9 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets the name of the output video codec.
         /// </summary>
+        /// <param name="state">Encording state.</param>
+        /// <param name="encodingOptions">Encoding options.</param>
+        /// <returns>Encoder string.</returns>
         public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
         {
             var codec = state.OutputVideoCodec;
@@ -315,6 +315,11 @@ namespace MediaBrowser.Controller.MediaEncoding
             return container;
         }
 
+        /// <summary>
+        /// Gets decoder from a codec.
+        /// </summary>
+        /// <param name="codec">Codec to use.</param>
+        /// <returns>Decoder string.</returns>
         public string GetDecoderFromCodec(string codec)
         {
             // For these need to find out the ffmpeg names
@@ -344,6 +349,8 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Infers the audio codec based on the url.
         /// </summary>
+        /// <param name="container">Container to use.</param>
+        /// <returns>Codec string.</returns>
         public string InferAudioCodec(string container)
         {
             var ext = "." + (container ?? string.Empty);
@@ -489,6 +496,9 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets the input argument.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="encodingOptions">Encoding options.</param>
+        /// <returns>Input arguments.</returns>
         public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
         {
             var arg = new StringBuilder();
@@ -965,6 +975,11 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets the video bitrate to specify on the command line.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="videoEncoder">Video encoder to use.</param>
+        /// <param name="encodingOptions">Encoding options.</param>
+        /// <param name="defaultPreset">Default present to use for encoding.</param>
+        /// <returns>Video bitrate.</returns>
         public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset)
         {
             var param = string.Empty;
@@ -1966,8 +1981,12 @@ namespace MediaBrowser.Controller.MediaEncoding
         }
 
         /// <summary>
-        /// Gets the graphical subtitle param.
+        /// Gets the graphical subtitle parameter.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="options">Encoding options.</param>
+        /// <param name="outputVideoCodec">Video codec to use.</param>
+        /// <returns>Graphical subtitle parameter.</returns>
         public string GetGraphicalSubtitleParam(
             EncodingJobInfo state,
             EncodingOptions options,
@@ -2485,6 +2504,13 @@ namespace MediaBrowser.Controller.MediaEncoding
             return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam);
         }
 
+        /// <summary>
+        /// Gets the output size parameter.
+        /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="options">Encoding options.</param>
+        /// <param name="outputVideoCodec">Video codec to use.</param>
+        /// <returns>The output size parameter.</returns>
         public string GetOutputSizeParam(
             EncodingJobInfo state,
             EncodingOptions options,
@@ -2495,8 +2521,13 @@ namespace MediaBrowser.Controller.MediaEncoding
         }
 
         /// <summary>
+        /// Gets the output size parameter.
         /// If we're going to put a fixed size on the command line, this will calculate it.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="options">Encoding options.</param>
+        /// <param name="outputVideoCodec">Video codec to use.</param>
+        /// <returns>The output size parameter.</returns>
         public string GetOutputSizeParamInternal(
             EncodingJobInfo state,
             EncodingOptions options,
@@ -2908,6 +2939,10 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets the number of threads.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="encodingOptions">Encoding options.</param>
+        /// <param name="outputVideoCodec">Video codec to use.</param>
+        /// <returns>Number of threads.</returns>
 #nullable enable
         public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
         {
@@ -3551,6 +3586,11 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets a hw decoder name.
         /// </summary>
+        /// <param name="options">Encoding options.</param>
+        /// <param name="decoder">Decoder to use.</param>
+        /// <param name="videoCodec">Video codec to use.</param>
+        /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+        /// <returns>Hardware decoder name.</returns>
         public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
         {
             var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoder) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
@@ -3569,6 +3609,11 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system.
         /// </summary>
+        /// <param name="state">Encoding state.</param>
+        /// <param name="options">Encoding options.</param>
+        /// <param name="videoCodec">Video codec to use.</param>
+        /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+        /// <returns>Hardware accelerator type.</returns>
         public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
         {
             var isWindows = OperatingSystem.IsWindows();

+ 218 - 218
MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1401
 
 using System;
 using System.Collections.Generic;
@@ -20,6 +20,44 @@ namespace MediaBrowser.Controller.MediaEncoding
     // For now, a common base class until the API and MediaEncoding classes are unified
     public class EncodingJobInfo
     {
+        public int? OutputAudioBitrate;
+        public int? OutputAudioChannels;
+
+        private TranscodeReason[] _transcodeReasons = null;
+
+        public EncodingJobInfo(TranscodingJobType jobType)
+        {
+            TranscodingType = jobType;
+            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            SupportedAudioCodecs = Array.Empty<string>();
+            SupportedVideoCodecs = Array.Empty<string>();
+            SupportedSubtitleCodecs = Array.Empty<string>();
+        }
+
+        public TranscodeReason[] TranscodeReasons
+        {
+            get
+            {
+                if (_transcodeReasons == null)
+                {
+                    if (BaseRequest.TranscodeReasons == null)
+                    {
+                        return Array.Empty<TranscodeReason>();
+                    }
+
+                    _transcodeReasons = BaseRequest.TranscodeReasons
+                        .Split(',')
+                        .Where(i => !string.IsNullOrEmpty(i))
+                        .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
+                        .ToArray();
+                }
+
+                return _transcodeReasons;
+            }
+        }
+
+        public IProgress<double> Progress { get; set; }
+
         public MediaStream VideoStream { get; set; }
 
         public VideoType VideoType { get; set; }
@@ -58,40 +96,6 @@ namespace MediaBrowser.Controller.MediaEncoding
 
         public string MimeType { get; set; }
 
-        public string GetMimeType(string outputPath, bool enableStreamDefault = true)
-        {
-            if (!string.IsNullOrEmpty(MimeType))
-            {
-                return MimeType;
-            }
-
-            return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
-        }
-
-        private TranscodeReason[] _transcodeReasons = null;
-
-        public TranscodeReason[] TranscodeReasons
-        {
-            get
-            {
-                if (_transcodeReasons == null)
-                {
-                    if (BaseRequest.TranscodeReasons == null)
-                    {
-                        return Array.Empty<TranscodeReason>();
-                    }
-
-                    _transcodeReasons = BaseRequest.TranscodeReasons
-                        .Split(',')
-                        .Where(i => !string.IsNullOrEmpty(i))
-                        .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
-                        .ToArray();
-                }
-
-                return _transcodeReasons;
-            }
-        }
-
         public bool IgnoreInputDts => MediaSource.IgnoreDts;
 
         public bool IgnoreInputIndex => MediaSource.IgnoreIndex;
@@ -144,196 +148,17 @@ namespace MediaBrowser.Controller.MediaEncoding
 
         public BaseEncodingJobOptions BaseRequest { get; set; }
 
-        public long? StartTimeTicks => BaseRequest.StartTimeTicks;
-
-        public bool CopyTimestamps => BaseRequest.CopyTimestamps;
-
-        public int? OutputAudioBitrate;
-        public int? OutputAudioChannels;
-
-        public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
-        {
-            var videoStream = VideoStream;
-            var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
-
-            if (!isInputInterlaced)
-            {
-                return false;
-            }
-
-            // Support general param
-            if (BaseRequest.DeInterlace)
-            {
-                return true;
-            }
-
-            if (!string.IsNullOrEmpty(videoCodec))
-            {
-                if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-            }
-
-            return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced;
-        }
-
-        public string[] GetRequestedProfiles(string codec)
-        {
-            if (!string.IsNullOrEmpty(BaseRequest.Profile))
-            {
-                return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
-            }
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                var profile = BaseRequest.GetOption(codec, "profile");
-
-                if (!string.IsNullOrEmpty(profile))
-                {
-                    return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
-                }
-            }
-
-            return Array.Empty<string>();
-        }
-
-        public string GetRequestedLevel(string codec)
-        {
-            if (!string.IsNullOrEmpty(BaseRequest.Level))
-            {
-                return BaseRequest.Level;
-            }
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                return BaseRequest.GetOption(codec, "level");
-            }
-
-            return null;
-        }
-
-        public int? GetRequestedMaxRefFrames(string codec)
-        {
-            if (BaseRequest.MaxRefFrames.HasValue)
-            {
-                return BaseRequest.MaxRefFrames;
-            }
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                var value = BaseRequest.GetOption(codec, "maxrefframes");
-                if (!string.IsNullOrEmpty(value)
-                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
-                {
-                    return result;
-                }
-            }
-
-            return null;
-        }
-
-        public int? GetRequestedVideoBitDepth(string codec)
-        {
-            if (BaseRequest.MaxVideoBitDepth.HasValue)
-            {
-                return BaseRequest.MaxVideoBitDepth;
-            }
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                var value = BaseRequest.GetOption(codec, "videobitdepth");
-                if (!string.IsNullOrEmpty(value)
-                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
-                {
-                    return result;
-                }
-            }
-
-            return null;
-        }
-
-        public int? GetRequestedAudioBitDepth(string codec)
-        {
-            if (BaseRequest.MaxAudioBitDepth.HasValue)
-            {
-                return BaseRequest.MaxAudioBitDepth;
-            }
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                var value = BaseRequest.GetOption(codec, "audiobitdepth");
-                if (!string.IsNullOrEmpty(value)
-                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
-                {
-                    return result;
-                }
-            }
-
-            return null;
-        }
-
-        public int? GetRequestedAudioChannels(string codec)
-        {
-            if (!string.IsNullOrEmpty(codec))
-            {
-                var value = BaseRequest.GetOption(codec, "audiochannels");
-                if (!string.IsNullOrEmpty(value)
-                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
-                {
-                    return result;
-                }
-            }
-
-            if (BaseRequest.MaxAudioChannels.HasValue)
-            {
-                return BaseRequest.MaxAudioChannels;
-            }
-
-            if (BaseRequest.AudioChannels.HasValue)
-            {
-                return BaseRequest.AudioChannels;
-            }
-
-            if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
-            {
-                return BaseRequest.TranscodingMaxAudioChannels;
-            }
-
-            return null;
-        }
-
         public bool IsVideoRequest { get; set; }
 
         public TranscodingJobType TranscodingType { get; set; }
 
-        public EncodingJobInfo(TranscodingJobType jobType)
-        {
-            TranscodingType = jobType;
-            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            SupportedAudioCodecs = Array.Empty<string>();
-            SupportedVideoCodecs = Array.Empty<string>();
-            SupportedSubtitleCodecs = Array.Empty<string>();
-        }
+        public long? StartTimeTicks => BaseRequest.StartTimeTicks;
+
+        public bool CopyTimestamps => BaseRequest.CopyTimestamps;
 
         public bool IsSegmentedLiveStream
             => TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue;
 
-        public bool EnableBreakOnNonKeyFrames(string videoCodec)
-        {
-            if (TranscodingType != TranscodingJobType.Progressive)
-            {
-                if (IsSegmentedLiveStream)
-                {
-                    return false;
-                }
-
-                return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
-            }
-
-            return false;
-        }
-
         public int? TotalOutputBitrate => (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
 
         public int? OutputWidth
@@ -682,6 +507,21 @@ namespace MediaBrowser.Controller.MediaEncoding
 
         public int HlsListSize => 0;
 
+        public bool EnableBreakOnNonKeyFrames(string videoCodec)
+        {
+            if (TranscodingType != TranscodingJobType.Progressive)
+            {
+                if (IsSegmentedLiveStream)
+                {
+                    return false;
+                }
+
+                return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
+            }
+
+            return false;
+        }
+
         private int? GetMediaStreamCount(MediaStreamType type, int limit)
         {
             var count = MediaSource.GetStreamCount(type);
@@ -694,7 +534,167 @@ namespace MediaBrowser.Controller.MediaEncoding
             return count;
         }
 
-        public IProgress<double> Progress { get; set; }
+        public string GetMimeType(string outputPath, bool enableStreamDefault = true)
+        {
+            if (!string.IsNullOrEmpty(MimeType))
+            {
+                return MimeType;
+            }
+
+            return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
+        }
+
+        public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
+        {
+            var videoStream = VideoStream;
+            var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
+
+            if (!isInputInterlaced)
+            {
+                return false;
+            }
+
+            // Support general param
+            if (BaseRequest.DeInterlace)
+            {
+                return true;
+            }
+
+            if (!string.IsNullOrEmpty(videoCodec))
+            {
+                if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced;
+        }
+
+        public string[] GetRequestedProfiles(string codec)
+        {
+            if (!string.IsNullOrEmpty(BaseRequest.Profile))
+            {
+                return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+            }
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                var profile = BaseRequest.GetOption(codec, "profile");
+
+                if (!string.IsNullOrEmpty(profile))
+                {
+                    return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+                }
+            }
+
+            return Array.Empty<string>();
+        }
+
+        public string GetRequestedLevel(string codec)
+        {
+            if (!string.IsNullOrEmpty(BaseRequest.Level))
+            {
+                return BaseRequest.Level;
+            }
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                return BaseRequest.GetOption(codec, "level");
+            }
+
+            return null;
+        }
+
+        public int? GetRequestedMaxRefFrames(string codec)
+        {
+            if (BaseRequest.MaxRefFrames.HasValue)
+            {
+                return BaseRequest.MaxRefFrames;
+            }
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                var value = BaseRequest.GetOption(codec, "maxrefframes");
+                if (!string.IsNullOrEmpty(value)
+                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        public int? GetRequestedVideoBitDepth(string codec)
+        {
+            if (BaseRequest.MaxVideoBitDepth.HasValue)
+            {
+                return BaseRequest.MaxVideoBitDepth;
+            }
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                var value = BaseRequest.GetOption(codec, "videobitdepth");
+                if (!string.IsNullOrEmpty(value)
+                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        public int? GetRequestedAudioBitDepth(string codec)
+        {
+            if (BaseRequest.MaxAudioBitDepth.HasValue)
+            {
+                return BaseRequest.MaxAudioBitDepth;
+            }
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                var value = BaseRequest.GetOption(codec, "audiobitdepth");
+                if (!string.IsNullOrEmpty(value)
+                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        public int? GetRequestedAudioChannels(string codec)
+        {
+            if (!string.IsNullOrEmpty(codec))
+            {
+                var value = BaseRequest.GetOption(codec, "audiochannels");
+                if (!string.IsNullOrEmpty(value)
+                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+                {
+                    return result;
+                }
+            }
+
+            if (BaseRequest.MaxAudioChannels.HasValue)
+            {
+                return BaseRequest.MaxAudioChannels;
+            }
+
+            if (BaseRequest.AudioChannels.HasValue)
+            {
+                return BaseRequest.AudioChannels;
+            }
+
+            if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+            {
+                return BaseRequest.TranscodingMaxAudioChannels;
+            }
+
+            return null;
+        }
 
         public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
         {

+ 7 - 0
MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs

@@ -16,6 +16,13 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Refreshes the chapter images.
         /// </summary>
+        /// <param name="video">Video to use.</param>
+        /// <param name="directoryService">Directory service to use.</param>
+        /// <param name="chapters">Set of chapters to refresh.</param>
+        /// <param name="extractImages">Option to extract images.</param>
+        /// <param name="saveChapters">Option to save chapters.</param>
+        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+        /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
         Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
     }
 }

+ 43 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -71,13 +71,42 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Extracts the video image.
         /// </summary>
+        /// <param name="inputFile">Input file.</param>
+        /// <param name="container">Video container type.</param>
+        /// <param name="mediaSource">Media source information.</param>
+        /// <param name="videoStream">Media stream information.</param>
+        /// <param name="threedFormat">Video 3D format.</param>
+        /// <param name="offset">Time offset.</param>
+        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+        /// <returns>Location of video image.</returns>
         Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
 
+        /// <summary>
+        /// Extracts the video image.
+        /// </summary>
+        /// <param name="inputFile">Input file.</param>
+        /// <param name="container">Video container type.</param>
+        /// <param name="mediaSource">Media source information.</param>
+        /// <param name="imageStream">Media stream information.</param>
+        /// <param name="imageStreamIndex">Index of the stream to extract from.</param>
+        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+        /// <returns>Location of video image.</returns>
         Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
 
         /// <summary>
         /// Extracts the video images on interval.
         /// </summary>
+        /// <param name="inputFile">Input file.</param>
+        /// <param name="container">Video container type.</param>
+        /// <param name="videoStream">Media stream information.</param>
+        /// <param name="mediaSource">Media source information.</param>
+        /// <param name="threedFormat">Video 3D format.</param>
+        /// <param name="interval">Time interval.</param>
+        /// <param name="targetDirectory">Directory to write images.</param>
+        /// <param name="filenamePrefix">Filename prefix to use.</param>
+        /// <param name="maxWidth">Maximum width of image.</param>
+        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+        /// <returns>A task.</returns>
         Task ExtractVideoImagesOnInterval(
             string inputFile,
             string container,
@@ -122,10 +151,24 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.String.</returns>
         string EscapeSubtitleFilterPath(string path);
 
+        /// <summary>
+        /// Sets the path to find FFmpeg.
+        /// </summary>
         void SetFFmpegPath();
 
+        /// <summary>
+        /// Updates the encoder path.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="pathType">The type of path.</param>
         void UpdateEncoderPath(string path, string pathType);
 
+        /// <summary>
+        /// Gets the primary playlist of .vob files.
+        /// </summary>
+        /// <param name="path">The to the .vob files.</param>
+        /// <param name="titleNumber">The title number to start with.</param>
+        /// <returns>A playlist.</returns>
         IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
     }
 }

+ 8 - 0
MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs

@@ -15,6 +15,14 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Gets the subtitles.
         /// </summary>
+        /// <param name="item">Item to use.</param>
+        /// <param name="mediaSourceId">Media source.</param>
+        /// <param name="subtitleStreamIndex">Subtitle stream to use.</param>
+        /// <param name="outputFormat">Output format to use.</param>
+        /// <param name="startTimeTicks">Start time.</param>
+        /// <param name="endTimeTicks">End time.</param>
+        /// <param name="preserveOriginalTimestamps">Option to preserve original timestamps.</param>
+        /// <param name="cancellationToken">The cancellation token for the operation.</param>
         /// <returns>Task{Stream}.</returns>
         Task<Stream> GetSubtitles(
             BaseItem item,

+ 16 - 16
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -1,6 +1,6 @@
 #nullable disable
 
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1306, SA1401
 
 using System;
 using System.Collections.Generic;
@@ -30,6 +30,21 @@ namespace MediaBrowser.Controller.Net
         private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections =
             new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
 
+        /// <summary>
+        /// The logger.
+        /// </summary>
+        protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+
+        protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
+        {
+            if (logger == null)
+            {
+                throw new ArgumentNullException(nameof(logger));
+            }
+
+            Logger = logger;
+        }
+
         /// <summary>
         /// Gets the type used for the messages sent to the client.
         /// </summary>
@@ -54,21 +69,6 @@ namespace MediaBrowser.Controller.Net
         /// <returns>Task{`1}.</returns>
         protected abstract Task<TReturnDataType> GetDataToSend();
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
-
-        protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
-        {
-            if (logger == null)
-            {
-                throw new ArgumentNullException(nameof(logger));
-            }
-
-            Logger = logger;
-        }
-
         /// <summary>
         /// Processes the message.
         /// </summary>

+ 0 - 1
MediaBrowser.Controller/Persistence/IUserDataRepository.cs

@@ -18,7 +18,6 @@ namespace MediaBrowser.Controller.Persistence
         /// <param name="key">The key.</param>
         /// <param name="userData">The user data.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
         void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
 
         /// <summary>

+ 39 - 39
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -31,24 +31,18 @@ namespace MediaBrowser.Controller.Playlists
             ".zpl"
         };
 
-        public Guid OwnerUserId { get; set; }
-
-        public Share[] Shares { get; set; }
-
         public Playlist()
         {
             Shares = Array.Empty<Share>();
         }
 
+        public Guid OwnerUserId { get; set; }
+
+        public Share[] Shares { get; set; }
+
         [JsonIgnore]
         public bool IsFile => IsPlaylistFile(Path);
 
-        public static bool IsPlaylistFile(string path)
-        {
-            // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
-            return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
-        }
-
         [JsonIgnore]
         public override string ContainingFolderPath
         {
@@ -80,6 +74,41 @@ namespace MediaBrowser.Controller.Playlists
         [JsonIgnore]
         public override bool SupportsCumulativeRunTimeTicks => true;
 
+        [JsonIgnore]
+        public override bool IsPreSorted => true;
+
+        public string PlaylistMediaType { get; set; }
+
+        [JsonIgnore]
+        public override string MediaType => PlaylistMediaType;
+
+        [JsonIgnore]
+        private bool IsSharedItem
+        {
+            get
+            {
+                var path = Path;
+
+                if (string.IsNullOrEmpty(path))
+                {
+                    return false;
+                }
+
+                return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
+            }
+        }
+
+        public static bool IsPlaylistFile(string path)
+        {
+            // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
+            return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
+        }
+
+        public void SetMediaType(string value)
+        {
+            PlaylistMediaType = value;
+        }
+
         public override double GetDefaultPrimaryImageAspectRatio()
         {
             return 1;
@@ -197,35 +226,6 @@ namespace MediaBrowser.Controller.Playlists
             return new[] { item };
         }
 
-        [JsonIgnore]
-        public override bool IsPreSorted => true;
-
-        public string PlaylistMediaType { get; set; }
-
-        [JsonIgnore]
-        public override string MediaType => PlaylistMediaType;
-
-        public void SetMediaType(string value)
-        {
-            PlaylistMediaType = value;
-        }
-
-        [JsonIgnore]
-        private bool IsSharedItem
-        {
-            get
-            {
-                var path = Path;
-
-                if (string.IsNullOrEmpty(path))
-                {
-                    return false;
-                }
-
-                return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
-            }
-        }
-
         public override bool IsVisible(User user)
         {
             if (!IsSharedItem)

+ 1 - 1
MediaBrowser.Controller/Providers/IDirectoryService.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CA1002, CS1591
+#pragma warning disable CA1002, CA1819, CS1591
 
 using System.Collections.Generic;
 using MediaBrowser.Model.IO;

+ 10 - 10
MediaBrowser.Controller/Resolvers/IItemResolver.cs

@@ -13,18 +13,18 @@ namespace MediaBrowser.Controller.Resolvers
     /// </summary>
     public interface IItemResolver
     {
+        /// <summary>
+        /// Gets the priority.
+        /// </summary>
+        /// <value>The priority.</value>
+        ResolverPriority Priority { get; }
+
         /// <summary>
         /// Resolves the path.
         /// </summary>
         /// <param name="args">The args.</param>
         /// <returns>BaseItem.</returns>
         BaseItem ResolvePath(ItemResolveArgs args);
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        ResolverPriority Priority { get; }
     }
 
     public interface IMultiItemResolver
@@ -38,14 +38,14 @@ namespace MediaBrowser.Controller.Resolvers
 
     public class MultiItemResolverResult
     {
-        public List<BaseItem> Items { get; set; }
-
-        public List<FileSystemMetadata> ExtraFiles { get; set; }
-
         public MultiItemResolverResult()
         {
             Items = new List<BaseItem>();
             ExtraFiles = new List<FileSystemMetadata>();
         }
+
+        public List<BaseItem> Items { get; set; }
+
+        public List<FileSystemMetadata> ExtraFiles { get; set; }
     }
 }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.