浏览代码

Merge pull request #21 from jellyfin/master

nightly
artiume 5 年之前
父节点
当前提交
697aee5b0c
共有 80 个文件被更改,包括 825 次插入1385 次删除
  1. 12 0
      Emby.Drawing/Emby.Drawing.csproj
  2. 34 314
      Emby.Drawing/ImageProcessor.cs
  3. 3 10
      Emby.Naming/Audio/AudioFileParser.cs
  4. 1 1
      Emby.Naming/Common/NamingOptions.cs
  5. 24 20
      Emby.Naming/TV/SeasonPathParser.cs
  6. 1 1
      Emby.Naming/Video/ExtraResolver.cs
  7. 2 2
      Emby.Naming/Video/StackResolver.cs
  8. 30 22
      Emby.Notifications/Api/NotificationsService.cs
  9. 3 0
      Emby.Notifications/CoreNotificationTypes.cs
  10. 14 0
      Emby.Notifications/Emby.Notifications.csproj
  11. 4 1
      Emby.Notifications/NotificationConfigurationFactory.cs
  12. 80 40
      Emby.Notifications/NotificationEntryPoint.cs
  13. 28 11
      Emby.Notifications/NotificationManager.cs
  14. 1 0
      Emby.Photos/Emby.Photos.csproj
  15. 4 6
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  16. 16 4
      Emby.Server.Implementations/ApplicationHost.cs
  17. 24 25
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  18. 5 10
      Emby.Server.Implementations/Devices/DeviceManager.cs
  19. 12 35
      Emby.Server.Implementations/Dto/DtoService.cs
  20. 16 18
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  21. 2 7
      Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
  22. 3 12
      Emby.Server.Implementations/EntryPoints/StartupWizard.cs
  23. 1 5
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  24. 23 21
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  25. 1 1
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  26. 15 12
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  27. 2 0
      Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
  28. 21 19
      Emby.Server.Implementations/IO/FileRefresher.cs
  29. 26 31
      Emby.Server.Implementations/Library/LibraryManager.cs
  30. 2 4
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  31. 5 7
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  32. 5 2
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  33. 2 1
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  34. 7 7
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  35. 1 2
      Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
  36. 5 7
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  37. 2 16
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  38. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  39. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  40. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  41. 5 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
  42. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
  43. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  44. 13 5
      Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
  45. 4 0
      Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
  46. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  47. 3 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  48. 5 2
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  49. 3 0
      Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
  50. 3 0
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  51. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  52. 22 26
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  53. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  54. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  55. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  56. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  57. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  58. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  59. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  60. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  61. 27 9
      Emby.Server.Implementations/Localization/Core/is.json
  62. 1 0
      Emby.Server.Implementations/Localization/Core/nn.json
  63. 123 110
      Emby.Server.Implementations/Session/SessionManager.cs
  64. 9 11
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  65. 3 8
      Jellyfin.Server/Program.cs
  66. 5 21
      MediaBrowser.Api/Images/ImageService.cs
  67. 2 2
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  68. 0 35
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  69. 0 2
      MediaBrowser.Controller/Drawing/ImageHelper.cs
  70. 0 3
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  71. 3 5
      MediaBrowser.Controller/Entities/Photo.cs
  72. 2 4
      MediaBrowser.Controller/Library/ILibraryManager.cs
  73. 3 0
      MediaBrowser.Controller/Library/IMediaSourceProvider.cs
  74. 2 10
      MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
  75. 0 61
      MediaBrowser.Controller/Providers/IImageEnhancer.cs
  76. 19 12
      MediaBrowser.Model/Drawing/ImageDimensions.cs
  77. 6 0
      MediaBrowser.Model/Querying/QueryResult.cs
  78. 2 0
      jellyfin.ruleset
  79. 70 380
      tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
  80. 13 3
      tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs

+ 12 - 0
Emby.Drawing/Emby.Drawing.csproj

@@ -17,4 +17,16 @@
     <Compile Include="..\SharedVersion.cs" />
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
   </ItemGroup>
 
 
+  <!-- Code analysers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
 </Project>
 </Project>

+ 34 - 314
Emby.Drawing/ImageProcessor.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
@@ -11,7 +10,6 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -23,7 +21,7 @@ namespace Emby.Drawing
     /// <summary>
     /// <summary>
     /// Class ImageProcessor.
     /// Class ImageProcessor.
     /// </summary>
     /// </summary>
-    public class ImageProcessor : IImageProcessor, IDisposable
+    public sealed class ImageProcessor : IImageProcessor, IDisposable
     {
     {
         // Increment this when there's a change requiring caches to be invalidated
         // Increment this when there's a change requiring caches to be invalidated
         private const string Version = "3";
         private const string Version = "3";
@@ -31,28 +29,24 @@ namespace Emby.Drawing
         private static readonly HashSet<string> _transparentImageTypes
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
 
 
-        /// <summary>
-        /// The _logger
-        /// </summary>
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
-        private IImageEncoder _imageEncoder;
+        private readonly IImageEncoder _imageEncoder;
         private readonly Func<ILibraryManager> _libraryManager;
         private readonly Func<ILibraryManager> _libraryManager;
         private readonly Func<IMediaEncoder> _mediaEncoder;
         private readonly Func<IMediaEncoder> _mediaEncoder;
 
 
-        private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         /// <summary>
         /// <summary>
-        ///
+        /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="logger"></param>
-        /// <param name="appPaths"></param>
-        /// <param name="fileSystem"></param>
-        /// <param name="imageEncoder"></param>
-        /// <param name="libraryManager"></param>
-        /// <param name="mediaEncoder"></param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="appPaths">The server application paths.</param>
+        /// <param name="fileSystem">The filesystem.</param>
+        /// <param name="imageEncoder">The image encoder.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="mediaEncoder">The media encoder.</param>
         public ImageProcessor(
         public ImageProcessor(
             ILogger<ImageProcessor> logger,
             ILogger<ImageProcessor> logger,
             IServerApplicationPaths appPaths,
             IServerApplicationPaths appPaths,
@@ -67,16 +61,10 @@ namespace Emby.Drawing
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _appPaths = appPaths;
-
-            ImageEnhancers = Array.Empty<IImageEnhancer>();
-
-            ImageHelper.ImageProcessor = this;
         }
         }
 
 
         private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
         private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
 
 
-        private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
-
         /// <inheritdoc />
         /// <inheritdoc />
         public IReadOnlyCollection<string> SupportedInputFormats =>
         public IReadOnlyCollection<string> SupportedInputFormats =>
             new HashSet<string>(StringComparer.OrdinalIgnoreCase)
             new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@@ -89,9 +77,7 @@ namespace Emby.Drawing
                 "aiff",
                 "aiff",
                 "cr2",
                 "cr2",
                 "crw",
                 "crw",
-
-                // Remove until supported
-                //"nef",
+                "nef",
                 "orf",
                 "orf",
                 "pef",
                 "pef",
                 "arw",
                 "arw",
@@ -110,19 +96,9 @@ namespace Emby.Drawing
                 "wbmp"
                 "wbmp"
             };
             };
 
 
-        /// <inheritdoc />
-        public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
         public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
 
 
-        /// <inheritdoc />
-        public IImageEncoder ImageEncoder
-        {
-            get => _imageEncoder;
-            set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
         public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
         {
         {
@@ -150,6 +126,8 @@ namespace Emby.Drawing
                 throw new ArgumentNullException(nameof(options));
                 throw new ArgumentNullException(nameof(options));
             }
             }
 
 
+            var libraryManager = _libraryManager();
+
             ItemImageInfo originalImage = options.Image;
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
             BaseItem item = options.Item;
 
 
@@ -157,9 +135,10 @@ namespace Emby.Drawing
             {
             {
                 if (item == null)
                 if (item == null)
                 {
                 {
-                    item = _libraryManager().GetItemById(options.ItemId);
+                    item = libraryManager.GetItemById(options.ItemId);
                 }
                 }
-                originalImage = await _libraryManager().ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
+
+                originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
             }
             }
 
 
             string originalImagePath = originalImage.Path;
             string originalImagePath = originalImage.Path;
@@ -186,27 +165,6 @@ namespace Emby.Drawing
             dateModified = supportedImageInfo.dateModified;
             dateModified = supportedImageInfo.dateModified;
             bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
             bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
 
 
-            if (options.Enhancers.Count > 0)
-            {
-                if (item == null)
-                {
-                    item = _libraryManager().GetItemById(options.ItemId);
-                }
-
-                var tuple = await GetEnhancedImage(new ItemImageInfo
-                {
-                    DateModified = dateModified,
-                    Type = originalImage.Type,
-                    Path = originalImagePath
-                }, requiresTransparency, item, options.ImageIndex, options.Enhancers, CancellationToken.None).ConfigureAwait(false);
-
-                originalImagePath = tuple.path;
-                dateModified = tuple.dateModified;
-                requiresTransparency = tuple.transparent;
-                // TODO: Get this info
-                originalImageSize = null;
-            }
-
             bool autoOrient = false;
             bool autoOrient = false;
             ImageOrientation? orientation = null;
             ImageOrientation? orientation = null;
             if (item is Photo photo)
             if (item is Photo photo)
@@ -239,12 +197,6 @@ namespace Emby.Drawing
             ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
             ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
             string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
             string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
 
 
-            CheckDisposed();
-
-            LockInfo lockInfo = GetLock(cacheFilePath);
-
-            await lockInfo.Lock.WaitAsync().ConfigureAwait(false);
-
             try
             try
             {
             {
                 if (!File.Exists(cacheFilePath))
                 if (!File.Exists(cacheFilePath))
@@ -270,10 +222,6 @@ namespace Emby.Drawing
                 _logger.LogError(ex, "Error encoding image");
                 _logger.LogError(ex, "Error encoding image");
                 return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
                 return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
             }
-            finally
-            {
-                ReleaseLock(cacheFilePath, lockInfo);
-            }
         }
         }
 
 
         private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
         private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
@@ -305,20 +253,18 @@ namespace Emby.Drawing
         }
         }
 
 
         private string GetMimeType(ImageFormat format, string path)
         private string GetMimeType(ImageFormat format, string path)
-        {
-            switch(format)
-            {
-                case ImageFormat.Bmp:  return MimeTypes.GetMimeType("i.bmp");
-                case ImageFormat.Gif:  return MimeTypes.GetMimeType("i.gif");
-                case ImageFormat.Jpg:  return MimeTypes.GetMimeType("i.jpg");
-                case ImageFormat.Png:  return MimeTypes.GetMimeType("i.png");
-                case ImageFormat.Webp: return MimeTypes.GetMimeType("i.webp");
-                default:               return MimeTypes.GetMimeType(path);
-            }
-        }
+            => format switch
+            {
+                ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
+                ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
+                ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
+                ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
+                ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
+                _ => MimeTypes.GetMimeType(path)
+            };
 
 
         /// <summary>
         /// <summary>
-        /// Gets the cache file path based on a set of parameters
+        /// Gets the cache file path based on a set of parameters.
         /// </summary>
         /// </summary>
         private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
         private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
         {
         {
@@ -400,11 +346,7 @@ namespace Emby.Drawing
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
-        {
-            var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray();
-
-            return GetImageCacheTag(item, image, supportedEnhancers);
-        }
+            => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
         public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
@@ -424,26 +366,6 @@ namespace Emby.Drawing
             }
             }
         }
         }
 
 
-        /// <inheritdoc />
-        public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers)
-        {
-            string originalImagePath = image.Path;
-            DateTime dateModified = image.DateModified;
-            ImageType imageType = image.Type;
-
-            // Optimization
-            if (imageEnhancers.Count == 0)
-            {
-                return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
-            }
-
-            // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
-            var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
-            cacheKeys.Add(originalImagePath + dateModified.Ticks);
-
-            return string.Join("|", cacheKeys).GetMD5().ToString("N", CultureInfo.InvariantCulture);
-        }
-
         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
         {
         {
             var inputFormat = Path.GetExtension(originalImagePath)
             var inputFormat = Path.GetExtension(originalImagePath)
@@ -487,154 +409,6 @@ namespace Emby.Drawing
             return (originalImagePath, dateModified);
             return (originalImagePath, dateModified);
         }
         }
 
 
-        /// <inheritdoc />
-        public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
-        {
-            var enhancers = GetSupportedEnhancers(item, imageType).ToArray();
-
-            ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex);
-
-            bool inputImageSupportsTransparency = SupportsTransparency(imageInfo.Path);
-
-            var result = await GetEnhancedImage(imageInfo, inputImageSupportsTransparency, item, imageIndex, enhancers, CancellationToken.None);
-
-            return result.path;
-        }
-
-        private async Task<(string path, DateTime dateModified, bool transparent)> GetEnhancedImage(
-            ItemImageInfo image,
-            bool inputImageSupportsTransparency,
-            BaseItem item,
-            int imageIndex,
-            IReadOnlyCollection<IImageEnhancer> enhancers,
-            CancellationToken cancellationToken)
-        {
-            var originalImagePath = image.Path;
-            var dateModified = image.DateModified;
-            var imageType = image.Type;
-
-            try
-            {
-                var cacheGuid = GetImageCacheTag(item, image, enhancers);
-
-                // Enhance if we have enhancers
-                var enhancedImageInfo = await GetEnhancedImageInternal(originalImagePath, item, imageType, imageIndex, enhancers, cacheGuid, cancellationToken).ConfigureAwait(false);
-
-                string enhancedImagePath = enhancedImageInfo.path;
-
-                // If the path changed update dateModified
-                if (!string.Equals(enhancedImagePath, originalImagePath, StringComparison.OrdinalIgnoreCase))
-                {
-                    var treatmentRequiresTransparency = enhancedImageInfo.transparent;
-
-                    return (enhancedImagePath, _fileSystem.GetLastWriteTimeUtc(enhancedImagePath), treatmentRequiresTransparency);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error enhancing image");
-            }
-
-            return (originalImagePath, dateModified, inputImageSupportsTransparency);
-        }
-
-        /// <summary>
-        /// Gets the enhanced image internal.
-        /// </summary>
-        /// <param name="originalImagePath">The original image path.</param>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="imageIndex">Index of the image.</param>
-        /// <param name="supportedEnhancers">The supported enhancers.</param>
-        /// <param name="cacheGuid">The cache unique identifier.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task&lt;System.String&gt;.</returns>
-        /// <exception cref="ArgumentNullException">
-        /// originalImagePath
-        /// or
-        /// item
-        /// </exception>
-        private async Task<(string path, bool transparent)> GetEnhancedImageInternal(
-            string originalImagePath,
-            BaseItem item,
-            ImageType imageType,
-            int imageIndex,
-            IReadOnlyCollection<IImageEnhancer> supportedEnhancers,
-            string cacheGuid,
-            CancellationToken cancellationToken = default)
-        {
-            if (string.IsNullOrEmpty(originalImagePath))
-            {
-                throw new ArgumentNullException(nameof(originalImagePath));
-            }
-
-            if (item == null)
-            {
-                throw new ArgumentNullException(nameof(item));
-            }
-
-            var treatmentRequiresTransparency = false;
-            foreach (var enhancer in supportedEnhancers)
-            {
-                if (!treatmentRequiresTransparency)
-                {
-                    treatmentRequiresTransparency = enhancer.GetEnhancedImageInfo(item, originalImagePath, imageType, imageIndex).RequiresTransparency;
-                }
-            }
-
-            // All enhanced images are saved as png to allow transparency
-            string cacheExtension = _imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp) ?
-                ".webp" :
-                (treatmentRequiresTransparency ? ".png" : ".jpg");
-
-            string enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + cacheExtension);
-
-            LockInfo lockInfo = GetLock(enhancedImagePath);
-
-            await lockInfo.Lock.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
-            {
-                // Check again in case of contention
-                if (File.Exists(enhancedImagePath))
-                {
-                    return (enhancedImagePath, treatmentRequiresTransparency);
-                }
-
-                Directory.CreateDirectory(Path.GetDirectoryName(enhancedImagePath));
-
-                await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false);
-
-                return (enhancedImagePath, treatmentRequiresTransparency);
-            }
-            finally
-            {
-                ReleaseLock(enhancedImagePath, lockInfo);
-            }
-        }
-
-        /// <summary>
-        /// Executes the image enhancers.
-        /// </summary>
-        /// <param name="imageEnhancers">The image enhancers.</param>
-        /// <param name="inputPath">The input path.</param>
-        /// <param name="outputPath">The output path.</param>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="imageIndex">Index of the image.</param>
-        /// <returns>Task{EnhancedImage}.</returns>
-        private static async Task ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, string inputPath, string outputPath, BaseItem item, ImageType imageType, int imageIndex)
-        {
-            // Run the enhancers sequentially in order of priority
-            foreach (var enhancer in imageEnhancers)
-            {
-                await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false);
-
-                // Feed the output into the next enhancer as input
-                inputPath = outputPath;
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the cache path.
         /// Gets the cache path.
         /// </summary>
         /// </summary>
@@ -647,7 +421,7 @@ namespace Emby.Drawing
         /// or
         /// or
         /// uniqueName
         /// uniqueName
         /// or
         /// or
-        /// fileExtension
+        /// fileExtension.
         /// </exception>
         /// </exception>
         public string GetCachePath(string path, string uniqueName, string fileExtension)
         public string GetCachePath(string path, string uniqueName, string fileExtension)
         {
         {
@@ -680,7 +454,7 @@ namespace Emby.Drawing
         /// <exception cref="ArgumentNullException">
         /// <exception cref="ArgumentNullException">
         /// path
         /// path
         /// or
         /// or
-        /// filename
+        /// filename.
         /// </exception>
         /// </exception>
         public string GetCachePath(string path, string filename)
         public string GetCachePath(string path, string filename)
         {
         {
@@ -688,6 +462,7 @@ namespace Emby.Drawing
             {
             {
                 throw new ArgumentNullException(nameof(path));
                 throw new ArgumentNullException(nameof(path));
             }
             }
+
             if (string.IsNullOrEmpty(filename))
             if (string.IsNullOrEmpty(filename))
             {
             {
                 throw new ArgumentNullException(nameof(filename));
                 throw new ArgumentNullException(nameof(filename));
@@ -709,74 +484,19 @@ namespace Emby.Drawing
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
-        {
-            foreach (var i in ImageEnhancers)
-            {
-                if (i.Supports(item, imageType))
-                {
-                    yield return i;
-                }
-            }
-        }
-
-
-        private class LockInfo
-        {
-            public SemaphoreSlim Lock = new SemaphoreSlim(1, 1);
-            public int Count = 1;
-        }
-
-        private LockInfo GetLock(string key)
-        {
-            lock (_locks)
-            {
-                if (_locks.TryGetValue(key, out LockInfo info))
-                {
-                    info.Count++;
-                }
-                else
-                {
-                    info = new LockInfo();
-                    _locks[key] = info;
-                }
-                return info;
-            }
-        }
-
-        private void ReleaseLock(string key, LockInfo info)
+        public void Dispose()
         {
         {
-            info.Lock.Release();
-
-            lock (_locks)
+            if (_disposed)
             {
             {
-                info.Count--;
-                if (info.Count <= 0)
-                {
-                    _locks.Remove(key);
-                    info.Lock.Dispose();
-                }
+                return;
             }
             }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _disposed = true;
 
 
-            var disposable = _imageEncoder as IDisposable;
-            if (disposable != null)
+            if (_imageEncoder is IDisposable disposable)
             {
             {
                 disposable.Dispose();
                 disposable.Dispose();
             }
             }
-        }
 
 
-        private void CheckDisposed()
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
+            _disposed = true;
         }
         }
     }
     }
 }
 }

+ 3 - 10
Emby.Naming/Audio/AudioFileParser.cs

@@ -8,19 +8,12 @@ using Emby.Naming.Common;
 
 
 namespace Emby.Naming.Audio
 namespace Emby.Naming.Audio
 {
 {
-    public class AudioFileParser
+    public static class AudioFileParser
     {
     {
-        private readonly NamingOptions _options;
-
-        public AudioFileParser(NamingOptions options)
-        {
-            _options = options;
-        }
-
-        public bool IsAudioFile(string path)
+        public static bool IsAudioFile(string path, NamingOptions options)
         {
         {
             var extension = Path.GetExtension(path) ?? string.Empty;
             var extension = Path.GetExtension(path) ?? string.Empty;
-            return _options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
         }
         }
     }
     }
 }
 }

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

@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 // "Foo Bar 889"
-                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },

+ 24 - 20
Emby.Naming/TV/SeasonPathParser.cs

@@ -4,7 +4,6 @@
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
-using System.Linq;
 
 
 namespace Emby.Naming.TV
 namespace Emby.Naming.TV
 {
 {
@@ -29,14 +28,14 @@ namespace Emby.Naming.TV
         {
         {
             var result = new SeasonPathParserResult();
             var result = new SeasonPathParserResult();
 
 
-            var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
+            var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
 
 
-            result.SeasonNumber = seasonNumberInfo.seasonNumber;
+            result.SeasonNumber = seasonNumber;
 
 
             if (result.SeasonNumber.HasValue)
             if (result.SeasonNumber.HasValue)
             {
             {
                 result.Success = true;
                 result.Success = true;
-                result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder;
+                result.IsSeasonFolder = isSeasonFolder;
             }
             }
 
 
             return result;
             return result;
@@ -90,12 +89,10 @@ namespace Emby.Naming.TV
             // Look for one of the season folder names
             // Look for one of the season folder names
             foreach (var name in _seasonFolderNames)
             foreach (var name in _seasonFolderNames)
             {
             {
-                var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase);
-
-                if (index != -1)
+                if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
                     var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
-                    if (result.Item1.HasValue)
+                    if (result.seasonNumber.HasValue)
                     {
                     {
                         return result;
                         return result;
                     }
                     }
@@ -105,25 +102,32 @@ namespace Emby.Naming.TV
             }
             }
 
 
             var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
             var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
-            var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue);
-            return (resultNumber, true);
+            for (int i = 0; i < parts.Length; i++)
+            {
+                if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
+                {
+                    return (seasonNumber, true);
+                }
+            }
+
+            return (null, true);
         }
         }
 
 
-        private static int? GetSeasonNumberFromPart(string part)
+        private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
         {
         {
+            seasonNumber = 0;
             if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
             if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
             {
             {
-                return null;
+                return false;
             }
             }
 
 
-            part = part.Substring(1);
-
-            if (int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+            if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
             {
             {
-                return value;
+                seasonNumber = value;
+                return true;
             }
             }
 
 
-            return null;
+            return false;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -131,7 +135,7 @@ namespace Emby.Naming.TV
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <returns>System.Nullable{System.Int32}.</returns>
         /// <returns>System.Nullable{System.Int32}.</returns>
-        private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(string path)
+        private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
         {
         {
             var numericStart = -1;
             var numericStart = -1;
             var length = 0;
             var length = 0;
@@ -142,7 +146,7 @@ namespace Emby.Naming.TV
             // Find out where the numbers start, and then keep going until they end
             // Find out where the numbers start, and then keep going until they end
             for (var i = 0; i < path.Length; i++)
             for (var i = 0; i < path.Length; i++)
             {
             {
-                if (char.IsNumber(path, i))
+                if (char.IsNumber(path[i]))
                 {
                 {
                     if (!hasOpenParenth)
                     if (!hasOpenParenth)
                     {
                     {
@@ -177,7 +181,7 @@ namespace Emby.Naming.TV
                 return (null, isSeasonFolder);
                 return (null, isSeasonFolder);
             }
             }
 
 
-            return (int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder);
+            return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder);
         }
         }
     }
     }
 }
 }

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

@@ -32,7 +32,7 @@ namespace Emby.Naming.Video
 
 
             if (rule.MediaType == MediaType.Audio)
             if (rule.MediaType == MediaType.Audio)
             {
             {
-                if (!new AudioFileParser(_options).IsAudioFile(path))
+                if (!AudioFileParser.IsAudioFile(path, _options))
                 {
                 {
                     return result;
                     return result;
                 }
                 }

+ 2 - 2
Emby.Naming/Video/StackResolver.cs

@@ -194,7 +194,7 @@ namespace Emby.Naming.Video
             }
             }
         }
         }
 
 
-        private string GetRegexInput(FileSystemMetadata file)
+        private static string GetRegexInput(FileSystemMetadata file)
         {
         {
             // For directories, dummy up an extension otherwise the expressions will fail
             // For directories, dummy up an extension otherwise the expressions will fail
             var input = !file.IsDirectory
             var input = !file.IsDirectory
@@ -204,7 +204,7 @@ namespace Emby.Naming.Video
             return Path.GetFileName(input);
             return Path.GetFileName(input);
         }
         }
 
 
-        private Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+        private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
         {
         {
             var regexInput = GetRegexInput(input);
             var regexInput = GetRegexInput(input);
 
 

+ 30 - 22
Emby.Notifications/Api/NotificationsService.cs

@@ -1,5 +1,11 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1402
+#pragma warning disable SA1600
+#pragma warning disable SA1649
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -16,7 +22,7 @@ namespace Emby.Notifications.Api
     public class GetNotifications : IReturn<NotificationResult>
     public class GetNotifications : IReturn<NotificationResult>
     {
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
 
         [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? IsRead { get; set; }
         public bool? IsRead { get; set; }
@@ -30,32 +36,34 @@ namespace Emby.Notifications.Api
 
 
     public class Notification
     public class Notification
     {
     {
-        public string Id { get; set; }
+        public string Id { get; set; } = string.Empty;
 
 
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
 
         public DateTime Date { get; set; }
         public DateTime Date { get; set; }
 
 
         public bool IsRead { get; set; }
         public bool IsRead { get; set; }
 
 
-        public string Name { get; set; }
+        public string Name { get; set; } = string.Empty;
 
 
-        public string Description { get; set; }
+        public string Description { get; set; } = string.Empty;
 
 
-        public string Url { get; set; }
+        public string Url { get; set; } = string.Empty;
 
 
         public NotificationLevel Level { get; set; }
         public NotificationLevel Level { get; set; }
     }
     }
 
 
     public class NotificationResult
     public class NotificationResult
     {
     {
-        public Notification[] Notifications { get; set; }
+        public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
+
         public int TotalRecordCount { get; set; }
         public int TotalRecordCount { get; set; }
     }
     }
 
 
     public class NotificationsSummary
     public class NotificationsSummary
     {
     {
         public int UnreadCount { get; set; }
         public int UnreadCount { get; set; }
+
         public NotificationLevel MaxUnreadNotificationLevel { get; set; }
         public NotificationLevel MaxUnreadNotificationLevel { get; set; }
     }
     }
 
 
@@ -63,7 +71,7 @@ namespace Emby.Notifications.Api
     public class GetNotificationsSummary : IReturn<NotificationsSummary>
     public class GetNotificationsSummary : IReturn<NotificationsSummary>
     {
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
     }
     }
 
 
     [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
     [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
@@ -80,16 +88,16 @@ namespace Emby.Notifications.Api
     public class AddAdminNotification : IReturnVoid
     public class AddAdminNotification : IReturnVoid
     {
     {
         [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
+        public string Name { get; set; } = string.Empty;
 
 
         [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Description { get; set; }
+        public string Description { get; set; } = string.Empty;
 
 
         [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ImageUrl { get; set; }
+        public string? ImageUrl { get; set; }
 
 
         [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Url { get; set; }
+        public string? Url { get; set; }
 
 
         [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public NotificationLevel Level { get; set; }
         public NotificationLevel Level { get; set; }
@@ -99,20 +107,20 @@ namespace Emby.Notifications.Api
     public class MarkRead : IReturnVoid
     public class MarkRead : IReturnVoid
     {
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
 
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
     }
 
 
     [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
     [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
     public class MarkUnread : IReturnVoid
     public class MarkUnread : IReturnVoid
     {
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
 
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
     }
 
 
     [Authenticated]
     [Authenticated]
@@ -127,32 +135,29 @@ namespace Emby.Notifications.Api
             _userManager = userManager;
             _userManager = userManager;
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationTypes request)
         public object Get(GetNotificationTypes request)
         {
         {
             return _notificationManager.GetNotificationTypes();
             return _notificationManager.GetNotificationTypes();
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationServices request)
         public object Get(GetNotificationServices request)
         {
         {
             return _notificationManager.GetNotificationServices().ToList();
             return _notificationManager.GetNotificationServices().ToList();
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationsSummary request)
         public object Get(GetNotificationsSummary request)
         {
         {
             return new NotificationsSummary
             return new NotificationsSummary
             {
             {
-
             };
             };
         }
         }
 
 
         public Task Post(AddAdminNotification request)
         public Task Post(AddAdminNotification request)
         {
         {
             // This endpoint really just exists as post of a real with sickbeard
             // This endpoint really just exists as post of a real with sickbeard
-            return AddNotification(request);
-        }
-
-        private Task AddNotification(AddAdminNotification request)
-        {
             var notification = new NotificationRequest
             var notification = new NotificationRequest
             {
             {
                 Date = DateTime.UtcNow,
                 Date = DateTime.UtcNow,
@@ -166,14 +171,17 @@ namespace Emby.Notifications.Api
             return _notificationManager.SendNotification(notification, CancellationToken.None);
             return _notificationManager.SendNotification(notification, CancellationToken.None);
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkRead request)
         public void Post(MarkRead request)
         {
         {
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkUnread request)
         public void Post(MarkUnread request)
         {
         {
         }
         }
 
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotifications request)
         public object Get(GetNotifications request)
         {
         {
             return new NotificationResult();
             return new NotificationResult();

+ 3 - 0
Emby.Notifications/CoreNotificationTypes.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;

+ 14 - 0
Emby.Notifications/Emby.Notifications.csproj

@@ -4,6 +4,8 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -16,4 +18,16 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
+  <!-- Code analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
 </Project>
 </Project>

+ 4 - 1
Emby.Notifications/NotificationConfigurationFactory.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Notifications;
 using MediaBrowser.Model.Notifications;
@@ -13,7 +16,7 @@ namespace Emby.Notifications
                 new ConfigurationStore
                 new ConfigurationStore
                 {
                 {
                     Key = "notifications",
                     Key = "notifications",
-                    ConfigurationType = typeof (NotificationOptions)
+                    ConfigurationType = typeof(NotificationOptions)
                 }
                 }
             };
             };
         }
         }

+ 80 - 40
Emby.Notifications/Notifications.cs → Emby.Notifications/NotificationEntryPoint.cs

@@ -21,70 +21,85 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Notifications
 namespace Emby.Notifications
 {
 {
     /// <summary>
     /// <summary>
-    /// Creates notifications for various system events
+    /// Creates notifications for various system events.
     /// </summary>
     /// </summary>
-    public class Notifications : IServerEntryPoint
+    public class NotificationEntryPoint : IServerEntryPoint
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-
+        private readonly IActivityManager _activityManager;
+        private readonly ILocalizationManager _localization;
         private readonly INotificationManager _notificationManager;
         private readonly INotificationManager _notificationManager;
-
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfigurationManager _config;
 
 
-        private Timer LibraryUpdateTimer { get; set; }
         private readonly object _libraryChangedSyncLock = new object();
         private readonly object _libraryChangedSyncLock = new object();
+        private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
 
 
-        private readonly IConfigurationManager _config;
-        private readonly ILocalizationManager _localization;
-        private readonly IActivityManager _activityManager;
+        private Timer? _libraryUpdateTimer;
 
 
         private string[] _coreNotificationTypes;
         private string[] _coreNotificationTypes;
 
 
-        public Notifications(
+        private bool _disposed = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        /// <param name="localization">The localization manager.</param>
+        /// <param name="notificationManager">The notification manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="appHost">The application host.</param>
+        /// <param name="config">The configuration manager.</param>
+        public NotificationEntryPoint(
+            ILogger<NotificationEntryPoint> logger,
             IActivityManager activityManager,
             IActivityManager activityManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
-            ILogger logger,
             INotificationManager notificationManager,
             INotificationManager notificationManager,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             IConfigurationManager config)
             IConfigurationManager config)
         {
         {
             _logger = logger;
             _logger = logger;
+            _activityManager = activityManager;
+            _localization = localization;
             _notificationManager = notificationManager;
             _notificationManager = notificationManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _appHost = appHost;
             _appHost = appHost;
             _config = config;
             _config = config;
-            _localization = localization;
-            _activityManager = activityManager;
 
 
             _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
             _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
         }
         }
 
 
+        /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
-            _libraryManager.ItemAdded += _libraryManager_ItemAdded;
-            _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
-            _activityManager.EntryCreated += _activityManager_EntryCreated;
+            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
+            _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
+            _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
+            _activityManager.EntryCreated += OnActivityManagerEntryCreated;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private async void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
         {
         {
             var type = NotificationType.ServerRestartRequired.ToString();
             var type = NotificationType.ServerRestartRequired.ToString();
 
 
             var notification = new NotificationRequest
             var notification = new NotificationRequest
             {
             {
                 NotificationType = type,
                 NotificationType = type,
-                Name = string.Format(_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), _appHost.Name)
+                Name = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
+                    _appHost.Name)
             };
             };
 
 
             await SendNotification(notification, null).ConfigureAwait(false);
             await SendNotification(notification, null).ConfigureAwait(false);
         }
         }
 
 
-        private async void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
         {
         {
             var entry = e.Argument;
             var entry = e.Argument;
 
 
@@ -117,7 +132,7 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
         }
 
 
-        private async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
+        private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
         {
         {
             if (!_appHost.HasUpdateAvailable)
             if (!_appHost.HasUpdateAvailable)
             {
             {
@@ -136,8 +151,7 @@ namespace Emby.Notifications
             await SendNotification(notification, null).ConfigureAwait(false);
             await SendNotification(notification, null).ConfigureAwait(false);
         }
         }
 
 
-        private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
-        private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
             {
             {
@@ -146,14 +160,17 @@ namespace Emby.Notifications
 
 
             lock (_libraryChangedSyncLock)
             lock (_libraryChangedSyncLock)
             {
             {
-                if (LibraryUpdateTimer == null)
+                if (_libraryUpdateTimer == null)
                 {
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, 5000,
-                                                   Timeout.Infinite);
+                    _libraryUpdateTimer = new Timer(
+                        LibraryUpdateTimerCallback,
+                        null,
+                        5000,
+                        Timeout.Infinite);
                 }
                 }
                 else
                 else
                 {
                 {
-                    LibraryUpdateTimer.Change(5000, Timeout.Infinite);
+                    _libraryUpdateTimer.Change(5000, Timeout.Infinite);
                 }
                 }
 
 
                 _itemsAdded.Add(e.Item);
                 _itemsAdded.Add(e.Item);
@@ -188,7 +205,8 @@ namespace Emby.Notifications
             {
             {
                 items = _itemsAdded.ToList();
                 items = _itemsAdded.ToList();
                 _itemsAdded.Clear();
                 _itemsAdded.Clear();
-                DisposeLibraryUpdateTimer();
+                _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
+                _libraryUpdateTimer = null;
             }
             }
 
 
             items = items.Take(10).ToList();
             items = items.Take(10).ToList();
@@ -198,7 +216,10 @@ namespace Emby.Notifications
                 var notification = new NotificationRequest
                 var notification = new NotificationRequest
                 {
                 {
                     NotificationType = NotificationType.NewLibraryContent.ToString(),
                     NotificationType = NotificationType.NewLibraryContent.ToString(),
-                    Name = string.Format(_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), GetItemName(item)),
+                    Name = string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
+                        GetItemName(item)),
                     Description = item.Overview
                     Description = item.Overview
                 };
                 };
 
 
@@ -206,6 +227,11 @@ namespace Emby.Notifications
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Creates a human readable name for the item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>A human readable name for the item.</returns>
         public static string GetItemName(BaseItem item)
         public static string GetItemName(BaseItem item)
         {
         {
             var name = item.Name;
             var name = item.Name;
@@ -219,6 +245,7 @@ namespace Emby.Notifications
                         episode.IndexNumber.Value,
                         episode.IndexNumber.Value,
                         name);
                         name);
                 }
                 }
+
                 if (episode.ParentIndexNumber.HasValue)
                 if (episode.ParentIndexNumber.HasValue)
                 {
                 {
                     name = string.Format(
                     name = string.Format(
@@ -229,7 +256,6 @@ namespace Emby.Notifications
                 }
                 }
             }
             }
 
 
-
             if (item is IHasSeries hasSeries)
             if (item is IHasSeries hasSeries)
             {
             {
                 name = hasSeries.SeriesName + " - " + name;
                 name = hasSeries.SeriesName + " - " + name;
@@ -257,7 +283,7 @@ namespace Emby.Notifications
             return name;
             return name;
         }
         }
 
 
-        private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem)
+        private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
         {
         {
             try
             try
             {
             {
@@ -269,23 +295,37 @@ namespace Emby.Notifications
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
-            DisposeLibraryUpdateTimer();
-
-            _libraryManager.ItemAdded -= _libraryManager_ItemAdded;
-            _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
-            _activityManager.EntryCreated -= _activityManager_EntryCreated;
+            Dispose(true);
+            GC.SuppressFinalize(this);
         }
         }
 
 
-        private void DisposeLibraryUpdateTimer()
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool disposing)
         {
         {
-            if (LibraryUpdateTimer != null)
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (disposing)
             {
             {
-                LibraryUpdateTimer.Dispose();
-                LibraryUpdateTimer = null;
+                _libraryUpdateTimer?.Dispose();
             }
             }
+
+            _libraryUpdateTimer = null;
+
+            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
+            _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
+            _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
+            _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
+
+            _disposed = true;
         }
         }
     }
     }
 }
 }

+ 28 - 11
Emby.Notifications/NotificationManager.cs

@@ -16,20 +16,32 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Notifications
 namespace Emby.Notifications
 {
 {
+    /// <summary>
+    /// NotificationManager class.
+    /// </summary>
     public class NotificationManager : INotificationManager
     public class NotificationManager : INotificationManager
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
-        private INotificationService[] _services;
-        private INotificationTypeFactory[] _typeFactories;
-
-        public NotificationManager(ILoggerFactory loggerFactory, IUserManager userManager, IServerConfigurationManager config)
+        private INotificationService[] _services = Array.Empty<INotificationService>();
+        private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NotificationManager" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="config">The server configuration manager.</param>
+        public NotificationManager(
+            ILogger<NotificationManager> logger,
+            IUserManager userManager,
+            IServerConfigurationManager config)
         {
         {
+            _logger = logger;
             _userManager = userManager;
             _userManager = userManager;
             _config = config;
             _config = config;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
         }
         }
 
 
         private NotificationOptions GetConfiguration()
         private NotificationOptions GetConfiguration()
@@ -37,12 +49,14 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
         public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
         {
         {
             return SendNotification(request, null, cancellationToken);
             return SendNotification(request, null, cancellationToken);
         }
         }
 
 
-        public Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
         {
         {
             var notificationType = request.NotificationType;
             var notificationType = request.NotificationType;
 
 
@@ -64,7 +78,8 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
             return Task.WhenAll(tasks);
         }
         }
 
 
-        private Task SendNotification(NotificationRequest request,
+        private Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             INotificationService service,
             IEnumerable<User> users,
             IEnumerable<User> users,
             string title,
             string title,
@@ -79,7 +94,7 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
             return Task.WhenAll(tasks);
         }
         }
 
 
-        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption options)
+        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
         {
         {
             if (request.SendToUserMode.HasValue)
             if (request.SendToUserMode.HasValue)
             {
             {
@@ -109,7 +124,8 @@ namespace Emby.Notifications
             return request.UserIds;
             return request.UserIds;
         }
         }
 
 
-        private async Task SendNotification(NotificationRequest request,
+        private async Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             INotificationService service,
             string title,
             string title,
             string description,
             string description,
@@ -161,12 +177,14 @@ namespace Emby.Notifications
             return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
             return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
         }
         }
 
 
+        /// <inheritdoc />
         public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
         public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
         {
         {
             _services = services.ToArray();
             _services = services.ToArray();
             _typeFactories = notificationTypeFactories.ToArray();
             _typeFactories = notificationTypeFactories.ToArray();
         }
         }
 
 
+        /// <inheritdoc />
         public List<NotificationTypeInfo> GetNotificationTypes()
         public List<NotificationTypeInfo> GetNotificationTypes()
         {
         {
             var list = _typeFactories.Select(i =>
             var list = _typeFactories.Select(i =>
@@ -180,7 +198,6 @@ namespace Emby.Notifications
                     _logger.LogError(ex, "Error in GetNotificationTypes");
                     _logger.LogError(ex, "Error in GetNotificationTypes");
                     return new List<NotificationTypeInfo>();
                     return new List<NotificationTypeInfo>();
                 }
                 }
-
             }).SelectMany(i => i).ToList();
             }).SelectMany(i => i).ToList();
 
 
             var config = GetConfiguration();
             var config = GetConfiguration();
@@ -193,13 +210,13 @@ namespace Emby.Notifications
             return list;
             return list;
         }
         }
 
 
+        /// <inheritdoc />
         public IEnumerable<NameIdPair> GetNotificationServices()
         public IEnumerable<NameIdPair> GetNotificationServices()
         {
         {
             return _services.Select(i => new NameIdPair
             return _services.Select(i => new NameIdPair
             {
             {
                 Name = i.Name,
                 Name = i.Name,
                 Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
                 Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
-
             }).OrderBy(i => i.Name);
             }).OrderBy(i => i.Name);
         }
         }
     }
     }

+ 1 - 0
Emby.Photos/Emby.Photos.csproj

@@ -17,6 +17,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

+ 4 - 6
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -29,7 +29,7 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Activity
 namespace Emby.Server.Implementations.Activity
 {
 {
-    public class ActivityLogEntryPoint : IServerEntryPoint
+    public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IInstallationManager _installationManager;
         private readonly IInstallationManager _installationManager;
@@ -39,7 +39,6 @@ namespace Emby.Server.Implementations.Activity
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
         private readonly ISubtitleManager _subManager;
         private readonly ISubtitleManager _subManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
-        private readonly IServerApplicationHost _appHost;
         private readonly IDeviceManager _deviceManager;
         private readonly IDeviceManager _deviceManager;
 
 
         /// <summary>
         /// <summary>
@@ -64,8 +63,7 @@ namespace Emby.Server.Implementations.Activity
             ILocalizationManager localization,
             ILocalizationManager localization,
             IInstallationManager installationManager,
             IInstallationManager installationManager,
             ISubtitleManager subManager,
             ISubtitleManager subManager,
-            IUserManager userManager,
-            IServerApplicationHost appHost)
+            IUserManager userManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
@@ -76,7 +74,6 @@ namespace Emby.Server.Implementations.Activity
             _installationManager = installationManager;
             _installationManager = installationManager;
             _subManager = subManager;
             _subManager = subManager;
             _userManager = userManager;
             _userManager = userManager;
-            _appHost = appHost;
         }
         }
 
 
         public Task RunAsync()
         public Task RunAsync()
@@ -141,7 +138,7 @@ namespace Emby.Server.Implementations.Activity
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     e.Provider,
                     e.Provider,
-                    Notifications.Notifications.GetItemName(e.Item)),
+                    Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
                 Type = "SubtitleDownloadFailure",
                 Type = "SubtitleDownloadFailure",
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ShortOverview = e.Exception.Message
                 ShortOverview = e.Exception.Message
@@ -533,6 +530,7 @@ namespace Emby.Server.Implementations.Activity
         private void CreateLogEntry(ActivityLogEntry entry)
         private void CreateLogEntry(ActivityLogEntry entry)
             => _activityManager.Create(entry);
             => _activityManager.Create(entry);
 
 
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
             _taskManager.TaskCompleted -= OnTaskCompleted;
             _taskManager.TaskCompleted -= OnTaskCompleted;

+ 16 - 4
Emby.Server.Implementations/ApplicationHost.cs

@@ -819,7 +819,18 @@ namespace Emby.Server.Implementations
             ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
             ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
             serviceCollection.AddSingleton(ChannelManager);
             serviceCollection.AddSingleton(ChannelManager);
 
 
-            SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager);
+            SessionManager = new SessionManager(
+                LoggerFactory.CreateLogger<SessionManager>(),
+                UserDataManager,
+                LibraryManager,
+                UserManager,
+                musicManager,
+                DtoService,
+                ImageProcessor,
+                this,
+                AuthenticationRepository,
+                DeviceManager,
+                MediaSourceManager);
             serviceCollection.AddSingleton(SessionManager);
             serviceCollection.AddSingleton(SessionManager);
 
 
             serviceCollection.AddSingleton<IDlnaManager>(
             serviceCollection.AddSingleton<IDlnaManager>(
@@ -836,7 +847,10 @@ namespace Emby.Server.Implementations
             UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
             UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
             serviceCollection.AddSingleton(UserViewManager);
             serviceCollection.AddSingleton(UserViewManager);
 
 
-            NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager);
+            NotificationManager = new NotificationManager(
+                LoggerFactory.CreateLogger<NotificationManager>(),
+                UserManager,
+                ServerConfigurationManager);
             serviceCollection.AddSingleton(NotificationManager);
             serviceCollection.AddSingleton(NotificationManager);
 
 
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
@@ -1074,8 +1088,6 @@ namespace Emby.Server.Implementations
                 GetExports<IMetadataSaver>(),
                 GetExports<IMetadataSaver>(),
                 GetExports<IExternalId>());
                 GetExports<IExternalId>());
 
 
-            ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>();
-
             LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
             LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
 
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());

+ 24 - 25
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3521,20 +3521,6 @@ namespace Emby.Server.Implementations.Data
             }
             }
 
 
             var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
             var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
-            if (includeTypes.Length == 1)
-            {
-                whereClauses.Add("type=@type");
-                if (statement != null)
-                {
-                    statement.TryBind("@type", includeTypes[0]);
-                }
-            }
-            else if (includeTypes.Length > 1)
-            {
-                var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
-                whereClauses.Add($"type in ({inClause})");
-            }
-
             // Only specify excluded types if no included types are specified
             // Only specify excluded types if no included types are specified
             if (includeTypes.Length == 0)
             if (includeTypes.Length == 0)
             {
             {
@@ -3553,6 +3539,19 @@ namespace Emby.Server.Implementations.Data
                     whereClauses.Add($"type not in ({inClause})");
                     whereClauses.Add($"type not in ({inClause})");
                 }
                 }
             }
             }
+            else if (includeTypes.Length == 1)
+            {
+                whereClauses.Add("type=@type");
+                if (statement != null)
+                {
+                    statement.TryBind("@type", includeTypes[0]);
+                }
+            }
+            else if (includeTypes.Length > 1)
+            {
+                var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
+                whereClauses.Add($"type in ({inClause})");
+            }
 
 
             if (query.ChannelIds.Length == 1)
             if (query.ChannelIds.Length == 1)
             {
             {
@@ -4927,7 +4926,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         // Not crazy about having this all the way down here, but at least it's in one place
         // Not crazy about having this all the way down here, but at least it's in one place
         readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
         readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
 
 
-        private IEnumerable<string> MapIncludeItemTypes(string value)
+        private string[] MapIncludeItemTypes(string value)
         {
         {
             if (_types.TryGetValue(value, out string[] result))
             if (_types.TryGetValue(value, out string[] result))
             {
             {
@@ -5611,32 +5610,32 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             return counts;
             return counts;
         }
         }
 
 
-        private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
+        private List<(int, string)> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
         {
         {
-            var list = new List<Tuple<int, string>>();
+            var list = new List<(int, string)>();
 
 
             if (item is IHasArtist hasArtist)
             if (item is IHasArtist hasArtist)
             {
             {
-                list.AddRange(hasArtist.Artists.Select(i => new Tuple<int, string>(0, i)));
+                list.AddRange(hasArtist.Artists.Select(i => (0, i)));
             }
             }
 
 
             if (item is IHasAlbumArtist hasAlbumArtist)
             if (item is IHasAlbumArtist hasAlbumArtist)
             {
             {
-                list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => new Tuple<int, string>(1, i)));
+                list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
             }
             }
 
 
-            list.AddRange(item.Genres.Select(i => new Tuple<int, string>(2, i)));
-            list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i)));
-            list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i)));
+            list.AddRange(item.Genres.Select(i => (2, i)));
+            list.AddRange(item.Studios.Select(i => (3, i)));
+            list.AddRange(item.Tags.Select(i => (4, i)));
 
 
             // keywords was 5
             // keywords was 5
 
 
-            list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i)));
+            list.AddRange(inheritedTags.Select(i => (6, i)));
 
 
             return list;
             return list;
         }
         }
 
 
-        private void UpdateItemValues(Guid itemId, List<Tuple<int, string>> values, IDatabaseConnection db)
+        private void UpdateItemValues(Guid itemId, List<(int, string)> values, IDatabaseConnection db)
         {
         {
             if (itemId.Equals(Guid.Empty))
             if (itemId.Equals(Guid.Empty))
             {
             {
@@ -5658,7 +5657,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             InsertItemValues(guidBlob, values, db);
             InsertItemValues(guidBlob, values, db);
         }
         }
 
 
-        private void InsertItemValues(byte[] idBlob, List<Tuple<int, string>> values, IDatabaseConnection db)
+        private void InsertItemValues(byte[] idBlob, List<(int, string)> values, IDatabaseConnection db)
         {
         {
             var startIndex = 0;
             var startIndex = 0;
             var limit = 100;
             var limit = 100;

+ 5 - 10
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -142,11 +142,10 @@ namespace Emby.Server.Implementations.Devices
 
 
         public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
         public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
         {
         {
-            var sessions = _authRepo.Get(new AuthenticationInfoQuery
+            IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
             {
             {
                 //UserId = query.UserId
                 //UserId = query.UserId
                 HasUser = true
                 HasUser = true
-
             }).Items;
             }).Items;
 
 
             // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
             // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
@@ -154,23 +153,19 @@ namespace Emby.Server.Implementations.Devices
             {
             {
                 var val = query.SupportsSync.Value;
                 var val = query.SupportsSync.Value;
 
 
-                sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val).ToArray();
+                sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
             }
             }
 
 
             if (!query.UserId.Equals(Guid.Empty))
             if (!query.UserId.Equals(Guid.Empty))
             {
             {
                 var user = _userManager.GetUserById(query.UserId);
                 var user = _userManager.GetUserById(query.UserId);
 
 
-                sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)).ToArray();
+                sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
             }
             }
 
 
             var array = sessions.Select(ToDeviceInfo).ToArray();
             var array = sessions.Select(ToDeviceInfo).ToArray();
 
 
-            return new QueryResult<DeviceInfo>
-            {
-                Items = array,
-                TotalRecordCount = array.Length
-            };
+            return new QueryResult<DeviceInfo>(array);
         }
         }
 
 
         private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
         private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
@@ -186,7 +181,7 @@ namespace Emby.Server.Implementations.Devices
                 LastUserName = authInfo.UserName,
                 LastUserName = authInfo.UserName,
                 Name = authInfo.DeviceName,
                 Name = authInfo.DeviceName,
                 DateLastActivity = authInfo.DateLastActivity,
                 DateLastActivity = authInfo.DateLastActivity,
-                IconUrl = caps == null ? null : caps.IconUrl
+                IconUrl = caps?.IconUrl
             };
             };
         }
         }
 
 

+ 12 - 35
Emby.Server.Implementations/Dto/DtoService.cs

@@ -1362,56 +1362,33 @@ namespace Emby.Server.Implementations.Dto
                 return null;
                 return null;
             }
             }
 
 
-            var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray();
-
             ImageDimensions size;
             ImageDimensions size;
 
 
             var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
             var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
 
 
             if (defaultAspectRatio > 0)
             if (defaultAspectRatio > 0)
             {
             {
-                if (supportedEnhancers.Length == 0)
-                {
-                    return defaultAspectRatio;
-                }
+                return defaultAspectRatio;
+            }
 
 
-                int dummyWidth = 200;
-                int dummyHeight = Convert.ToInt32(dummyWidth / defaultAspectRatio);
-                size = new ImageDimensions(dummyWidth, dummyHeight);
+            if (!imageInfo.IsLocalFile)
+            {
+                return null;
             }
             }
-            else
+
+            try
             {
             {
-                if (!imageInfo.IsLocalFile)
-                {
-                    return null;
-                }
+                size = _imageProcessor.GetImageDimensions(item, imageInfo);
 
 
-                try
+                if (size.Width <= 0 || size.Height <= 0)
                 {
                 {
-                    size = _imageProcessor.GetImageDimensions(item, imageInfo);
-
-                    if (size.Width <= 0 || size.Height <= 0)
-                    {
-                        return null;
-                    }
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
                     return null;
                     return null;
                 }
                 }
             }
             }
-
-            foreach (var enhancer in supportedEnhancers)
+            catch (Exception ex)
             {
             {
-                try
-                {
-                    size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error in image enhancer: {0}", enhancer.GetType().Name);
-                }
+                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
+                return null;
             }
             }
 
 
             var width = size.Width;
             var width = size.Width;

+ 16 - 18
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
-    public class RecordingNotifier : IServerEntryPoint
+    public sealed class RecordingNotifier : IServerEntryPoint
     {
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
@@ -28,32 +28,33 @@ namespace Emby.Server.Implementations.EntryPoints
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
         }
         }
 
 
+        /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
-            _liveTvManager.TimerCancelled += _liveTvManager_TimerCancelled;
-            _liveTvManager.SeriesTimerCancelled += _liveTvManager_SeriesTimerCancelled;
-            _liveTvManager.TimerCreated += _liveTvManager_TimerCreated;
-            _liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated;
+            _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled;
+            _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled;
+            _liveTvManager.TimerCreated += OnLiveTvManagerTimerCreated;
+            _liveTvManager.SeriesTimerCreated += OnLiveTvManagerSeriesTimerCreated;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private void _liveTvManager_SeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
             SendMessage("SeriesTimerCreated", e.Argument);
             SendMessage("SeriesTimerCreated", e.Argument);
         }
         }
 
 
-        private void _liveTvManager_TimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
             SendMessage("TimerCreated", e.Argument);
             SendMessage("TimerCreated", e.Argument);
         }
         }
 
 
-        private void _liveTvManager_SeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
             SendMessage("SeriesTimerCancelled", e.Argument);
             SendMessage("SeriesTimerCancelled", e.Argument);
         }
         }
 
 
-        private void _liveTvManager_TimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
             SendMessage("TimerCancelled", e.Argument);
             SendMessage("TimerCancelled", e.Argument);
         }
         }
@@ -64,11 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
             try
             try
             {
             {
-                await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None);
-            }
-            catch (ObjectDisposedException)
-            {
-                // TODO Log exception or Investigate and properly fix.
+                await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None).ConfigureAwait(false);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -76,12 +73,13 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
-            _liveTvManager.TimerCancelled -= _liveTvManager_TimerCancelled;
-            _liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled;
-            _liveTvManager.TimerCreated -= _liveTvManager_TimerCreated;
-            _liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated;
+            _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
+            _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
+            _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
+            _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
         }
         }
     }
     }
 }
 }

+ 2 - 7
Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
@@ -15,21 +14,17 @@ namespace Emby.Server.Implementations.EntryPoints
     /// </summary>
     /// </summary>
     public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
     public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
     {
     {
-        private readonly ILogger _logger;
-
         /// <summary>
         /// <summary>
         /// The user manager.
         /// The user manager.
         /// </summary>
         /// </summary>
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
-
-        private IFileSystem _fileSystem;
+        private readonly IFileSystem _fileSystem;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
         /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
         /// </summary>
         /// </summary>
-        public RefreshUsersMetadata(ILogger logger, IUserManager userManager, IFileSystem fileSystem)
+        public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
         {
         {
-            _logger = logger;
             _userManager = userManager;
             _userManager = userManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
         }
         }

+ 3 - 12
Emby.Server.Implementations/EntryPoints/StartupWizard.cs

@@ -3,37 +3,28 @@ using Emby.Server.Implementations.Browser;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
     /// <summary>
     /// <summary>
     /// Class StartupWizard.
     /// Class StartupWizard.
     /// </summary>
     /// </summary>
-    public class StartupWizard : IServerEntryPoint
+    public sealed class StartupWizard : IServerEntryPoint
     {
     {
         /// <summary>
         /// <summary>
         /// The app host.
         /// The app host.
         /// </summary>
         /// </summary>
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly ILogger _logger;
-
-        private IServerConfigurationManager _config;
+        private readonly IServerConfigurationManager _config;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="StartupWizard"/> class.
         /// Initializes a new instance of the <see cref="StartupWizard"/> class.
         /// </summary>
         /// </summary>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
-        /// <param name="logger">The logger.</param>
         /// <param name="config">The configuration manager.</param>
         /// <param name="config">The configuration manager.</param>
-        public StartupWizard(IServerApplicationHost appHost, ILogger logger, IServerConfigurationManager config)
+        public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
-            _logger = logger;
             _config = config;
             _config = config;
         }
         }
 
 

+ 1 - 5
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -3,8 +3,6 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using Emby.Server.Implementations.Udp;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
@@ -23,9 +21,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// The logger.
         /// The logger.
         /// </summary>
         /// </summary>
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly ISocketFactory _socketFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-        private readonly IJsonSerializer _json;
 
 
         /// <summary>
         /// <summary>
         /// The UDP server.
         /// The UDP server.
@@ -64,7 +60,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
             _cancellationTokenSource.Cancel();
             _cancellationTokenSource.Cancel();
             _udpServer.Dispose();
             _udpServer.Dispose();
-
+            _cancellationTokenSource.Dispose();
             _cancellationTokenSource = null;
             _cancellationTokenSource = null;
             _udpServer = null;
             _udpServer = null;
 
 

+ 23 - 21
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -13,39 +13,38 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
-    public class UserDataChangeNotifier : IServerEntryPoint
+    public sealed class UserDataChangeNotifier : IServerEntryPoint
     {
     {
+        private const int UpdateDuration = 500;
+
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
-        private readonly ILogger _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
 
 
+        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
+
         private readonly object _syncLock = new object();
         private readonly object _syncLock = new object();
-        private Timer UpdateTimer { get; set; }
-        private const int UpdateDuration = 500;
+        private Timer _updateTimer;
 
 
-        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
 
 
-        public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager)
+        public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
         {
         {
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
-            _logger = logger;
             _userManager = userManager;
             _userManager = userManager;
         }
         }
 
 
         public Task RunAsync()
         public Task RunAsync()
         {
         {
-            _userDataManager.UserDataSaved += _userDataManager_UserDataSaved;
+            _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e)
+        void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
         {
         {
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
             {
             {
@@ -54,14 +53,17 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
             lock (_syncLock)
             lock (_syncLock)
             {
             {
-                if (UpdateTimer == null)
+                if (_updateTimer == null)
                 {
                 {
-                    UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration,
-                                                   Timeout.Infinite);
+                    _updateTimer = new Timer(
+                        UpdateTimerCallback,
+                        null,
+                        UpdateDuration,
+                        Timeout.Infinite);
                 }
                 }
                 else
                 else
                 {
                 {
-                    UpdateTimer.Change(UpdateDuration, Timeout.Infinite);
+                    _updateTimer.Change(UpdateDuration, Timeout.Infinite);
                 }
                 }
 
 
                 if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
                 if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
@@ -97,10 +99,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
                 var task = SendNotifications(changes, CancellationToken.None);
                 var task = SendNotifications(changes, CancellationToken.None);
 
 
-                if (UpdateTimer != null)
+                if (_updateTimer != null)
                 {
                 {
-                    UpdateTimer.Dispose();
-                    UpdateTimer = null;
+                    _updateTimer.Dispose();
+                    _updateTimer = null;
                 }
                 }
             }
             }
         }
         }
@@ -145,13 +147,13 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         public void Dispose()
         public void Dispose()
         {
         {
-            if (UpdateTimer != null)
+            if (_updateTimer != null)
             {
             {
-                UpdateTimer.Dispose();
-                UpdateTimer = null;
+                _updateTimer.Dispose();
+                _updateTimer = null;
             }
             }
 
 
-            _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved;
+            _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
         }
         }
     }
     }
 }
 }

+ 1 - 1
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.HttpClientManager
             if (!string.IsNullOrWhiteSpace(userInfo))
             if (!string.IsNullOrWhiteSpace(userInfo))
             {
             {
                 _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
                 _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
-                url = url.Replace(userInfo + '@', string.Empty);
+                url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
             }
             }
 
 
             var request = new HttpRequestMessage(method, url);
             var request = new HttpRequestMessage(method, url);

+ 15 - 12
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -40,9 +40,9 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly string _defaultRedirectPath;
         private readonly string _defaultRedirectPath;
         private readonly string _baseUrlPrefix;
         private readonly string _baseUrlPrefix;
-        private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+        private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
         private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
         private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
+        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         public HttpListenerHost(
         public HttpListenerHost(
@@ -72,6 +72,8 @@ namespace Emby.Server.Implementations.HttpServer
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
         }
         }
 
 
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
         public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
         public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
 
 
         public static HttpListenerHost Instance { get; protected set; }
         public static HttpListenerHost Instance { get; protected set; }
@@ -82,8 +84,6 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         public ServiceController ServiceController { get; private set; }
         public ServiceController ServiceController { get; private set; }
 
 
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
         public object CreateInstance(Type type)
         public object CreateInstance(Type type)
         {
         {
             return _appHost.CreateInstance(type);
             return _appHost.CreateInstance(type);
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         private static string NormalizeUrlPath(string path)
         private static string NormalizeUrlPath(string path)
         {
         {
-            if (path.StartsWith("/"))
+            if (path.Length > 0 && path[0] == '/')
             {
             {
                 // If the path begins with a leading slash, just return it as-is
                 // If the path begins with a leading slash, just return it as-is
                 return path;
                 return path;
@@ -131,13 +131,13 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         public Type GetServiceTypeByRequest(Type requestType)
         public Type GetServiceTypeByRequest(Type requestType)
         {
         {
-            ServiceOperationsMap.TryGetValue(requestType, out var serviceType);
+            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
             return serviceType;
             return serviceType;
         }
         }
 
 
         public void AddServiceInfo(Type serviceType, Type requestType)
         public void AddServiceInfo(Type serviceType, Type requestType)
         {
         {
-            ServiceOperationsMap[requestType] = serviceType;
+            _serviceOperationsMap[requestType] = serviceType;
         }
         }
 
 
         private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
         private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
@@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.HttpServer
                 else
                 else
                 {
                 {
                     var inners = agg.InnerExceptions;
                     var inners = agg.InnerExceptions;
-                    if (inners != null && inners.Count > 0)
+                    if (inners.Count > 0)
                     {
                     {
                         return GetActualException(inners[0]);
                         return GetActualException(inners[0]);
                     }
                     }
@@ -362,7 +362,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return true;
                 return true;
             }
             }
 
 
-            host = host ?? string.Empty;
+            host ??= string.Empty;
 
 
             if (_networkManager.IsInPrivateAddressSpace(host))
             if (_networkManager.IsInPrivateAddressSpace(host))
             {
             {
@@ -433,7 +433,7 @@ namespace Emby.Server.Implementations.HttpServer
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Overridable method that can be used to implement a custom hnandler
+        /// Overridable method that can be used to implement a custom handler.
         /// </summary>
         /// </summary>
         public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         {
         {
@@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.HttpServer
                     || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
                     || string.IsNullOrEmpty(localPath)
                     || string.IsNullOrEmpty(localPath)
-                    || !localPath.StartsWith(_baseUrlPrefix))
+                    || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // 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 or missing
                     _logger.LogDebug("Normalizing a URL at {0}", localPath);
                     _logger.LogDebug("Normalizing a URL at {0}", localPath);
@@ -693,7 +693,10 @@ namespace Emby.Server.Implementations.HttpServer
 
 
         protected virtual void Dispose(bool disposing)
         protected virtual void Dispose(bool disposing)
         {
         {
-            if (_disposed) return;
+            if (_disposed)
+            {
+                return;
+            }
 
 
             if (disposing)
             if (disposing)
             {
             {

+ 2 - 0
Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs

@@ -6,7 +6,9 @@ namespace Emby.Server.Implementations.IO
     public class ExtendedFileSystemInfo
     public class ExtendedFileSystemInfo
     {
     {
         public bool IsHidden { get; set; }
         public bool IsHidden { get; set; }
+
         public bool IsReadOnly { get; set; }
         public bool IsReadOnly { get; set; }
+
         public bool Exists { get; set; }
         public bool Exists { get; set; }
     }
     }
 }
 }

+ 21 - 19
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -15,27 +15,29 @@ namespace Emby.Server.Implementations.IO
 {
 {
     public class FileRefresher : IDisposable
     public class FileRefresher : IDisposable
     {
     {
-        private ILogger Logger { get; set; }
-        private ILibraryManager LibraryManager { get; set; }
-        private IServerConfigurationManager ConfigurationManager { get; set; }
+        private readonly ILogger _logger;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _configurationManager;
+
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly List<string> _affectedPaths = new List<string>();
-        private Timer _timer;
         private readonly object _timerLock = new object();
         private readonly object _timerLock = new object();
-        public string Path { get; private set; }
-
-        public event EventHandler<EventArgs> Completed;
+        private Timer _timer;
 
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         {
         {
             logger.LogDebug("New file refresher created for {0}", path);
             logger.LogDebug("New file refresher created for {0}", path);
             Path = path;
             Path = path;
 
 
-            ConfigurationManager = configurationManager;
-            LibraryManager = libraryManager;
-            Logger = logger;
+            _configurationManager = configurationManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
             AddPath(path);
             AddPath(path);
         }
         }
 
 
+        public event EventHandler<EventArgs> Completed;
+
+        public string Path { get; private set; }
+
         private void AddAffectedPath(string path)
         private void AddAffectedPath(string path)
         {
         {
             if (string.IsNullOrEmpty(path))
             if (string.IsNullOrEmpty(path))
@@ -80,11 +82,11 @@ namespace Emby.Server.Implementations.IO
 
 
                 if (_timer == null)
                 if (_timer == null)
                 {
                 {
-                    _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
+                    _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
                 }
                 }
                 else
                 else
                 {
                 {
-                    _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
+                    _timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
                 }
                 }
             }
             }
         }
         }
@@ -93,7 +95,7 @@ namespace Emby.Server.Implementations.IO
         {
         {
             lock (_timerLock)
             lock (_timerLock)
             {
             {
-                Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
+                _logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
 
 
                 Path = path;
                 Path = path;
                 AddAffectedPath(path);
                 AddAffectedPath(path);
@@ -116,7 +118,7 @@ namespace Emby.Server.Implementations.IO
                 paths = _affectedPaths.ToList();
                 paths = _affectedPaths.ToList();
             }
             }
 
 
-            Logger.LogDebug("Timer stopped.");
+            _logger.LogDebug("Timer stopped.");
 
 
             DisposeTimer();
             DisposeTimer();
             Completed?.Invoke(this, EventArgs.Empty);
             Completed?.Invoke(this, EventArgs.Empty);
@@ -127,7 +129,7 @@ namespace Emby.Server.Implementations.IO
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
-                Logger.LogError(ex, "Error processing directory changes");
+                _logger.LogError(ex, "Error processing directory changes");
             }
             }
         }
         }
 
 
@@ -147,7 +149,7 @@ namespace Emby.Server.Implementations.IO
                     continue;
                     continue;
                 }
                 }
 
 
-                Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
 
 
                 try
                 try
                 {
                 {
@@ -158,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
                     // Should we remove it from it's parent?
-                    Logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    Logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
                 }
                 }
             }
             }
         }
         }
@@ -178,7 +180,7 @@ namespace Emby.Server.Implementations.IO
 
 
             while (item == null && !string.IsNullOrEmpty(path))
             while (item == null && !string.IsNullOrEmpty(path))
             {
             {
-                item = LibraryManager.FindByPath(path, null);
+                item = _libraryManager.FindByPath(path, null);
 
 
                 path = System.IO.Path.GetDirectoryName(path);
                 path = System.IO.Path.GetDirectoryName(path);
             }
             }

+ 26 - 31
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1174,7 +1174,6 @@ namespace Emby.Server.Implementations.Library
 
 
             return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
             return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
                 .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
                 .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
-                .OrderBy(i => i.Name)
                 .ToList();
                 .ToList();
         }
         }
 
 
@@ -1406,25 +1405,32 @@ namespace Emby.Server.Implementations.Library
 
 
         private void SetTopParentOrAncestorIds(InternalItemsQuery query)
         private void SetTopParentOrAncestorIds(InternalItemsQuery query)
         {
         {
-            if (query.AncestorIds.Length == 0)
+            var ancestorIds = query.AncestorIds;
+            int len = ancestorIds.Length;
+            if (len == 0)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList();
-
-            if (parents.All(i => i is ICollectionFolder || i is UserView))
+            var parents = new BaseItem[len];
+            for (int i = 0; i < len; i++)
             {
             {
-                // Optimize by querying against top level views
-                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
-                query.AncestorIds = Array.Empty<Guid>();
-
-                // Prevent searching in all libraries due to empty filter
-                if (query.TopParentIds.Length == 0)
+                parents[i] = GetItemById(ancestorIds[i]);
+                if (!(parents[i] is ICollectionFolder || parents[i] is UserView))
                 {
                 {
-                    query.TopParentIds = new[] { Guid.NewGuid() };
+                    return;
                 }
                 }
             }
             }
+
+            // Optimize by querying against top level views
+            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
+            query.AncestorIds = Array.Empty<Guid>();
+
+            // Prevent searching in all libraries due to empty filter
+            if (query.TopParentIds.Length == 0)
+            {
+                query.TopParentIds = new[] { Guid.NewGuid() };
+            }
         }
         }
 
 
         public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
         public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
@@ -1585,7 +1591,7 @@ namespace Emby.Server.Implementations.Library
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         {
         {
             var tasks = IntroProviders
             var tasks = IntroProviders
-                .OrderBy(i => i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1)
+                .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
                 .Take(1)
                 .Take(1)
                 .Select(i => GetIntros(i, item, user));
                 .Select(i => GetIntros(i, item, user));
 
 
@@ -2363,33 +2369,22 @@ namespace Emby.Server.Implementations.Library
             new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
             new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
         }
         }
 
 
-        public bool IsVideoFile(string path, LibraryOptions libraryOptions)
+        /// <inheritdoc />
+        public bool IsVideoFile(string path)
         {
         {
             var resolver = new VideoResolver(GetNamingOptions());
             var resolver = new VideoResolver(GetNamingOptions());
             return resolver.IsVideoFile(path);
             return resolver.IsVideoFile(path);
         }
         }
 
 
-        public bool IsVideoFile(string path)
-        {
-            return IsVideoFile(path, new LibraryOptions());
-        }
-
-        public bool IsAudioFile(string path, LibraryOptions libraryOptions)
-        {
-            var parser = new AudioFileParser(GetNamingOptions());
-            return parser.IsAudioFile(path);
-        }
-
+        /// <inheritdoc />
         public bool IsAudioFile(string path)
         public bool IsAudioFile(string path)
-        {
-            return IsAudioFile(path, new LibraryOptions());
-        }
+            => AudioFileParser.IsAudioFile(path, GetNamingOptions());
 
 
+        /// <inheritdoc />
         public int? GetSeasonNumberFromPath(string path)
         public int? GetSeasonNumberFromPath(string path)
-        {
-            return SeasonPathParser.Parse(path, true, true).SeasonNumber;
-        }
+            => SeasonPathParser.Parse(path, true, true).SeasonNumber;
 
 
+        /// <inheritdoc />
         public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
         public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
         {
         {
             var series = episode.Series;
             var series = episode.Series;

+ 2 - 4
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         {
         {
             // Return audio if the path is a file and has a matching extension
             // Return audio if the path is a file and has a matching extension
 
 
-            var libraryOptions = args.GetLibraryOptions();
             var collectionType = args.GetCollectionType();
             var collectionType = args.GetCollectionType();
 
 
             var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
             var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
@@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
                 return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
             }
             }
 
 
-            if (LibraryManager.IsAudioFile(args.Path, libraryOptions))
+            if (LibraryManager.IsAudioFile(args.Path))
             {
             {
                 var extension = Path.GetExtension(args.Path);
                 var extension = Path.GetExtension(args.Path);
 
 
@@ -105,7 +104,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
                 var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
 
 
                 // For conflicting extensions, give priority to videos
                 // For conflicting extensions, give priority to videos
-                if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions))
+                if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
                 {
                 {
                     return null;
                     return null;
                 }
                 }
@@ -121,7 +120,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 {
                 {
                     item = new MediaBrowser.Controller.Entities.Audio.Audio();
                     item = new MediaBrowser.Controller.Entities.Audio.Audio();
                 }
                 }
-
                 else if (isBooksCollectionType)
                 else if (isBooksCollectionType)
                 {
                 {
                     item = new AudioBook();
                     item = new AudioBook();

+ 5 - 7
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -78,9 +77,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// <summary>
         /// Determine if the supplied file data points to a music album.
         /// Determine if the supplied file data points to a music album.
         /// </summary>
         /// </summary>
-        public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions)
+        public bool IsMusicAlbum(string path, IDirectoryService directoryService)
         {
         {
-            return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, libraryOptions, _libraryManager);
+            return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -94,7 +93,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             if (args.IsDirectory)
             if (args.IsDirectory)
             {
             {
                 // if (args.Parent is MusicArtist) return true;  //saves us from testing children twice
                 // if (args.Parent is MusicArtist) return true;  //saves us from testing children twice
-                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, args.GetLibraryOptions(), _libraryManager))
+                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
                 {
                 {
                     return true;
                     return true;
                 }
                 }
@@ -112,7 +111,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             IDirectoryService directoryService,
             IDirectoryService directoryService,
             ILogger logger,
             ILogger logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            LibraryOptions libraryOptions,
             ILibraryManager libraryManager)
             ILibraryManager libraryManager)
         {
         {
             var discSubfolderCount = 0;
             var discSubfolderCount = 0;
@@ -132,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                         }
                         }
 
 
                         var path = fileSystemInfo.FullName;
                         var path = fileSystemInfo.FullName;
-                        var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
+                        var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
 
 
                         if (hasMusic)
                         if (hasMusic)
                         {
                         {
@@ -153,7 +151,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 {
                 {
                     var fullName = fileSystemInfo.FullName;
                     var fullName = fileSystemInfo.FullName;
 
 
-                    if (libraryManager.IsAudioFile(fullName, libraryOptions))
+                    if (libraryManager.IsAudioFile(fullName))
                     {
                     {
                         return true;
                         return true;
                     }
                     }

+ 5 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -80,14 +80,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             }
             }
 
 
             // Avoid mis-identifying top folders
             // Avoid mis-identifying top folders
-            if (args.Parent.IsRoot) return null;
+            if (args.Parent.IsRoot)
+            {
+                return null;
+            }
 
 
             var directoryService = args.DirectoryService;
             var directoryService = args.DirectoryService;
 
 
             var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
             var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
 
             // If we contain an album assume we are an artist folder
             // If we contain an album assume we are an artist folder
-            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
+            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
         }
         }
     }
     }
 }
 }

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                             };
                             };
                             break;
                             break;
                         }
                         }
+
                         if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
                         if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
                         {
                         {
                             videoInfo = parser.ResolveDirectory(args.Path);
                             videoInfo = parser.ResolveDirectory(args.Path);
@@ -137,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return null;
                     return null;
                 }
                 }
 
 
-                if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub)
+                if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
                 {
                 {
                     var path = args.Path;
                     var path = args.Path;
 
 

+ 7 - 7
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             if (result.Items.Count == 1)
             if (result.Items.Count == 1)
             {
             {
                 var videoPath = result.Items[0].Path;
                 var videoPath = result.Items[0].Path;
-                var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, libraryOptions, videoPath, i.Name));
+                var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
 
 
                 if (!hasPhotos)
                 if (!hasPhotos)
                 {
                 {
@@ -446,8 +446,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return movie;
                     return movie;
                 }
                 }
             }
             }
-
-            if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
+            else if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
             {
             {
                 return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
                 return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
             }
             }
@@ -519,14 +518,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 return null;
                 return null;
             }
             }
 
 
+            int additionalPartsLen = folderPaths.Count - 1;
+            var additionalParts = new string[additionalPartsLen];
+            folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen);
+
             var returnVideo = new T
             var returnVideo = new T
             {
             {
                 Path = folderPaths[0],
                 Path = folderPaths[0],
-
-                AdditionalParts = folderPaths.Skip(1).ToArray(),
-
+                AdditionalParts = additionalParts,
                 VideoType = videoTypes[0],
                 VideoType = videoTypes[0],
-
                 Name = result[0].Name
                 Name = result[0].Name
             };
             };
 
 

+ 1 - 2
Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs

@@ -63,13 +63,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
             {
             {
                 if (!file.IsDirectory && PhotoResolver.IsImageFile(file.FullName, _imageProcessor))
                 if (!file.IsDirectory && PhotoResolver.IsImageFile(file.FullName, _imageProcessor))
                 {
                 {
-                    var libraryOptions = args.GetLibraryOptions();
                     var filename = file.Name;
                     var filename = file.Name;
                     var ownedByMedia = false;
                     var ownedByMedia = false;
 
 
                     foreach (var siblingFile in files)
                     foreach (var siblingFile in files)
                     {
                     {
-                        if (PhotoResolver.IsOwnedByMedia(_libraryManager, libraryOptions, siblingFile.FullName, filename))
+                        if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
                         {
                         {
                             ownedByMedia = true;
                             ownedByMedia = true;
                             break;
                             break;

+ 5 - 7
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -8,7 +8,6 @@ using System.Linq;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace Emby.Server.Implementations.Library.Resolvers
 namespace Emby.Server.Implementations.Library.Resolvers
@@ -57,11 +56,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
 
                         // Make sure the image doesn't belong to a video file
                         // Make sure the image doesn't belong to a video file
                         var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
                         var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
-                        var libraryOptions = args.GetLibraryOptions();
 
 
                         foreach (var file in files)
                         foreach (var file in files)
                         {
                         {
-                            if (IsOwnedByMedia(_libraryManager, libraryOptions, file.FullName, filename))
+                            if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
                             {
                             {
                                 return null;
                                 return null;
                             }
                             }
@@ -78,17 +76,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
             return null;
             return null;
         }
         }
 
 
-        internal static bool IsOwnedByMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
+        internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
         {
         {
-            if (libraryManager.IsVideoFile(file, libraryOptions))
+            if (libraryManager.IsVideoFile(file))
             {
             {
-                return IsOwnedByResolvedMedia(libraryManager, libraryOptions, file, imageFilename);
+                return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
             }
             }
 
 
             return false;
             return false;
         }
         }
 
 
-        internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
+        internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
 
 
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)

+ 2 - 16
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                         return null;
                         return null;
                     }
                     }
 
 
-                    if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
+                    if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false))
                     {
                     {
                         return new Series
                         return new Series
                         {
                         {
@@ -123,24 +123,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             ILogger logger,
             ILogger logger,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
-            LibraryOptions libraryOptions,
             bool isTvContentType)
             bool isTvContentType)
         {
         {
             foreach (var child in fileSystemChildren)
             foreach (var child in fileSystemChildren)
             {
             {
-                //if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
-                //{
-                //    //logger.LogDebug("Igoring series file or folder marked hidden: {0}", child.FullName);
-                //    continue;
-                //}
-
-                // Can't enforce this because files saved by Bitcasa are always marked System
-                //if ((attributes & FileAttributes.System) == FileAttributes.System)
-                //{
-                //    logger.LogDebug("Igoring series subfolder marked system: {0}", child.FullName);
-                //    continue;
-                //}
-
                 if (child.IsDirectory)
                 if (child.IsDirectory)
                 {
                 {
                     if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
                     if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
@@ -152,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 else
                 else
                 {
                 {
                     string fullName = child.FullName;
                     string fullName = child.FullName;
-                    if (libraryManager.IsVideoFile(fullName, libraryOptions))
+                    if (libraryManager.IsVideoFile(fullName))
                     {
                     {
                         if (isTvContentType)
                         if (isTvContentType)
                         {
                         {

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Net.Http;
 using System.Net.Http;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -30,7 +30,6 @@ using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;

+ 5 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
 
 
@@ -5,11 +8,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
 {
     public class EntryPoint : IServerEntryPoint
     public class EntryPoint : IServerEntryPoint
     {
     {
+        /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
             return EmbyTV.Current.Start();
             return EmbyTV.Current.Start();
         }
         }
 
 
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
         }
         }

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;

+ 13 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
@@ -21,7 +24,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
                 if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
                 {
                 {
-                    name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
+                    name += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " S{0}E{1}",
+                        info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture),
+                        info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
                     addHyphen = false;
                     addHyphen = false;
                 }
                 }
                 else if (info.OriginalAirDate.HasValue)
                 else if (info.OriginalAirDate.HasValue)
@@ -32,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     }
                     else
                     else
                     {
                     {
-                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd");
+                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
                     }
                     }
                 }
                 }
                 else
                 else
@@ -67,14 +74,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         {
             date = date.ToLocalTime();
             date = date.ToLocalTime();
 
 
-            return string.Format("{0}_{1}_{2}_{3}_{4}_{5}",
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0}_{1}_{2}_{3}_{4}_{5}",
                 date.Year.ToString("0000", CultureInfo.InvariantCulture),
                 date.Year.ToString("0000", CultureInfo.InvariantCulture),
                 date.Month.ToString("00", CultureInfo.InvariantCulture),
                 date.Month.ToString("00", CultureInfo.InvariantCulture),
                 date.Day.ToString("00", CultureInfo.InvariantCulture),
                 date.Day.ToString("00", CultureInfo.InvariantCulture),
                 date.Hour.ToString("00", CultureInfo.InvariantCulture),
                 date.Hour.ToString("00", CultureInfo.InvariantCulture),
                 date.Minute.ToString("00", CultureInfo.InvariantCulture),
                 date.Minute.ToString("00", CultureInfo.InvariantCulture),
-                date.Second.ToString("00", CultureInfo.InvariantCulture)
-                );
+                date.Second.ToString("00", CultureInfo.InvariantCulture));
         }
         }
     }
     }
 }
 }

+ 4 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
@@ -12,6 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         {
         }
         }
 
 
+        /// <inheritdoc />
         public override void Add(SeriesTimerInfo item)
         public override void Add(SeriesTimerInfo item)
         {
         {
             if (string.IsNullOrEmpty(item.Id))
             if (string.IsNullOrEmpty(item.Id))

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Globalization;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 5 - 2
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
@@ -91,12 +94,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 {
                 {
                     using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
                     using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
                     {
                     {
-                        await gzStream.CopyToAsync(fileStream).ConfigureAwait(false);
+                        await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
                     }
                     }
                 }
                 }
                 else
                 else
                 {
                 {
-                    await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 

+ 3 - 0
Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;

+ 3 - 0
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;

+ 1 - 1
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,5 +1,5 @@
-#pragma warning disable SA1600
 #pragma warning disable CS1591
 #pragma warning disable CS1591
+#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 22 - 26
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -1,52 +1,48 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv
 namespace Emby.Server.Implementations.LiveTv
 {
 {
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     {
     {
+        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+        private const char StreamIdDelimeter = '_';
+        private const string StreamIdDelimeterString = "_";
+
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
-        private IApplicationPaths _appPaths;
 
 
-        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
+        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
         {
         {
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
-            _mediaEncoder = mediaEncoder;
             _appHost = appHost;
             _appHost = appHost;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _appPaths = appPaths;
         }
         }
 
 
         public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
         public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
         {
         {
-            var baseItem = (BaseItem)item;
-
-            if (baseItem.SourceType == SourceType.LiveTV)
+            if (item.SourceType == SourceType.LiveTV)
             {
             {
                 var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
                 var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
 
 
-                if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null)
+                if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null)
                 {
                 {
                     return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
                     return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
                 }
                 }
@@ -55,10 +51,6 @@ namespace Emby.Server.Implementations.LiveTv
             return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
             return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
         }
         }
 
 
-        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char StreamIdDelimeter = '_';
-        private const string StreamIdDelimeterString = "_";
-
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
         {
         {
             IEnumerable<MediaSourceInfo> sources;
             IEnumerable<MediaSourceInfo> sources;
@@ -91,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv
             foreach (var source in list)
             foreach (var source in list)
             {
             {
                 source.Type = MediaSourceType.Default;
                 source.Type = MediaSourceType.Default;
-                source.BufferMs = source.BufferMs ?? 1500;
+                source.BufferMs ??= 1500;
 
 
                 if (source.RequiresOpening || forceRequireOpening)
                 if (source.RequiresOpening || forceRequireOpening)
                 {
                 {
@@ -100,11 +92,14 @@ namespace Emby.Server.Implementations.LiveTv
 
 
                 if (source.RequiresOpening)
                 if (source.RequiresOpening)
                 {
                 {
-                    var openKeys = new List<string>();
-                    openKeys.Add(item.GetType().Name);
-                    openKeys.Add(item.Id.ToString("N", CultureInfo.InvariantCulture));
-                    openKeys.Add(source.Id ?? string.Empty);
-                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
+                    var openKeys = new List<string>
+                    {
+                        item.GetType().Name,
+                        item.Id.ToString("N", CultureInfo.InvariantCulture),
+                        source.Id ?? string.Empty
+                    };
+
+                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
                 }
                 }
 
 
                 // Dummy this up so that direct play checks can still run
                 // Dummy this up so that direct play checks can still run
@@ -114,11 +109,12 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 }
             }
             }
 
 
-            _logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+            _logger.LogDebug("MediaSources: {@MediaSources}", list);
 
 
             return list;
             return list;
         }
         }
 
 
+        /// <inheritdoc />
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
         {
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Buffers;
 using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;

+ 27 - 9
Emby.Server.Implementations/Localization/Core/is.json

@@ -3,7 +3,7 @@
     "ItemRemovedWithName": "{0} var fjarlægt úr safninu",
     "ItemRemovedWithName": "{0} var fjarlægt úr safninu",
     "ItemAddedWithName": "{0} var bætt í safnið",
     "ItemAddedWithName": "{0} var bætt í safnið",
     "Inherit": "Erfa",
     "Inherit": "Erfa",
-    "HomeVideos": "Myndbönd að heiman",
+    "HomeVideos": "Heimamyndbönd",
     "HeaderRecordingGroups": "Upptökuhópar",
     "HeaderRecordingGroups": "Upptökuhópar",
     "HeaderNextUp": "Næst á dagskrá",
     "HeaderNextUp": "Næst á dagskrá",
     "HeaderLiveTV": "Sjónvarp í beinni útsendingu",
     "HeaderLiveTV": "Sjónvarp í beinni útsendingu",
@@ -36,10 +36,10 @@
     "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
     "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
     "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
     "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
     "NotificationOptionUserLockedOut": "Notandi læstur úti",
     "NotificationOptionUserLockedOut": "Notandi læstur úti",
-    "NotificationOptionServerRestartRequired": "Endurræsing miðlara nauðsynileg",
+    "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
     "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
     "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
     "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
     "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
-    "NotificationOptionPluginInstalled": "Viðbót settur upp",
+    "NotificationOptionPluginInstalled": "Viðbót sett upp",
     "NotificationOptionPluginError": "Bilun í viðbót",
     "NotificationOptionPluginError": "Bilun í viðbót",
     "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
     "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
     "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
     "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
@@ -50,15 +50,15 @@
     "NameSeasonUnknown": "Sería óþekkt",
     "NameSeasonUnknown": "Sería óþekkt",
     "NameSeasonNumber": "Sería {0}",
     "NameSeasonNumber": "Sería {0}",
     "MixedContent": "Blandað efni",
     "MixedContent": "Blandað efni",
-    "MessageServerConfigurationUpdated": "Stillingar  miðlarans hefur verið uppfærð",
-    "MessageApplicationUpdatedTo": "Jellyfin Server hefur verið uppfærður í {0}",
-    "MessageApplicationUpdated": "Jellyfin Server hefur verið uppfærður",
+    "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
+    "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
+    "MessageApplicationUpdated": "Jellyfin þjónn hefur verið uppfærður",
     "Latest": "Nýjasta",
     "Latest": "Nýjasta",
-    "LabelRunningTimeValue": "Keyrslutími kerfis: {0}",
+    "LabelRunningTimeValue": "spilunartími: {0}",
     "User": "Notandi",
     "User": "Notandi",
     "System": "Kerfi",
     "System": "Kerfi",
     "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
     "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
-    "NewVersionIsAvailable": "Ný útgáfa af Jellyfin Server er fáanleg til niðurhals.",
+    "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
     "NameInstallFailed": "{0} uppsetning mistókst",
     "NameInstallFailed": "{0} uppsetning mistókst",
     "MusicVideos": "Tónlistarmyndbönd",
     "MusicVideos": "Tónlistarmyndbönd",
     "Music": "Tónlist",
     "Music": "Tónlist",
@@ -74,5 +74,23 @@
     "PluginUpdatedWithName": "{0} var uppfært",
     "PluginUpdatedWithName": "{0} var uppfært",
     "PluginUninstalledWithName": "{0} var fjarlægt",
     "PluginUninstalledWithName": "{0} var fjarlægt",
     "PluginInstalledWithName": "{0} var sett upp",
     "PluginInstalledWithName": "{0} var sett upp",
-    "NotificationOptionTaskFailed": "Tímasett verkefni mistókst"
+    "NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
+    "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
+    "VersionNumber": "Útgáfa {0}",
+    "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
+    "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
+    "UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}",
+    "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}",
+    "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
+    "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
+    "UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
+    "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur",
+    "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
+    "SubtitlesDownloadedForItem": "Skjátextum halað niður fyrir {0}",
+    "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
+    "ProviderValue": "Veitandi: {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
+    "ValueSpecialEpisodeName": "Sérstakt - {0}",
+    "Shows": "Þættir",
+    "Playlists": "Spilunarlisti"
 }
 }

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

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

+ 123 - 110
Emby.Server.Implementations/Session/SessionManager.cs

@@ -62,8 +62,43 @@ namespace Emby.Server.Implementations.Session
         private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
         private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
             new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
             new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
 
 
+        private Timer _idleTimer;
+
+        private DtoOptions _itemInfoDtoOptions;
+        private bool _disposed = false;
+
+        public SessionManager(
+            ILogger<SessionManager> logger,
+            IUserDataManager userDataManager,
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IMusicManager musicManager,
+            IDtoService dtoService,
+            IImageProcessor imageProcessor,
+            IServerApplicationHost appHost,
+            IAuthenticationRepository authRepo,
+            IDeviceManager deviceManager,
+            IMediaSourceManager mediaSourceManager)
+        {
+            _logger = logger;
+            _userDataManager = userDataManager;
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _musicManager = musicManager;
+            _dtoService = dtoService;
+            _imageProcessor = imageProcessor;
+            _appHost = appHost;
+            _authRepo = authRepo;
+            _deviceManager = deviceManager;
+            _mediaSourceManager = mediaSourceManager;
+
+            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
+        }
+
+        /// <inheritdoc />
         public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
         public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
 
 
+        /// <inheritdoc />
         public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
         public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
 
 
         /// <summary>
         /// <summary>
@@ -81,40 +116,23 @@ namespace Emby.Server.Implementations.Session
         /// </summary>
         /// </summary>
         public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
         public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> SessionStarted;
         public event EventHandler<SessionEventArgs> SessionStarted;
 
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> CapabilitiesChanged;
         public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> SessionEnded;
         public event EventHandler<SessionEventArgs> SessionEnded;
 
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> SessionActivity;
         public event EventHandler<SessionEventArgs> SessionActivity;
 
 
-        public SessionManager(
-            IUserDataManager userDataManager,
-            ILoggerFactory loggerFactory,
-            ILibraryManager libraryManager,
-            IUserManager userManager,
-            IMusicManager musicManager,
-            IDtoService dtoService,
-            IImageProcessor imageProcessor,
-            IServerApplicationHost appHost,
-            IAuthenticationRepository authRepo,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager)
-        {
-            _userDataManager = userDataManager;
-            _logger = loggerFactory.CreateLogger(nameof(SessionManager));
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-            _musicManager = musicManager;
-            _dtoService = dtoService;
-            _imageProcessor = imageProcessor;
-            _appHost = appHost;
-            _authRepo = authRepo;
-            _deviceManager = deviceManager;
-            _mediaSourceManager = mediaSourceManager;
-            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
-        }
+        /// <summary>
+        /// Gets all connections.
+        /// </summary>
+        /// <value>All connections.</value>
+        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
 
 
         private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
         private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
         {
         {
@@ -135,14 +153,17 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
-        private bool _disposed = false;
-
+        /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
             Dispose(true);
             Dispose(true);
             GC.SuppressFinalize(this);
             GC.SuppressFinalize(this);
         }
         }
 
 
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         protected virtual void Dispose(bool disposing)
         {
         {
             if (_disposed)
             if (_disposed)
@@ -152,15 +173,17 @@ namespace Emby.Server.Implementations.Session
 
 
             if (disposing)
             if (disposing)
             {
             {
-                // TODO: dispose stuff
+                _idleTimer?.Dispose();
             }
             }
 
 
+            _idleTimer = null;
+
             _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
             _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 
 
             _disposed = true;
             _disposed = true;
         }
         }
 
 
-        public void CheckDisposed()
+        private void CheckDisposed()
         {
         {
             if (_disposed)
             if (_disposed)
             {
             {
@@ -168,12 +191,6 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Gets all connections.
-        /// </summary>
-        /// <value>All connections.</value>
-        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
-
         private void OnSessionStarted(SessionInfo info)
         private void OnSessionStarted(SessionInfo info)
         {
         {
             if (!string.IsNullOrEmpty(info.DeviceId))
             if (!string.IsNullOrEmpty(info.DeviceId))
@@ -204,13 +221,13 @@ namespace Emby.Server.Implementations.Session
                 new SessionEventArgs
                 new SessionEventArgs
                 {
                 {
                     SessionInfo = info
                     SessionInfo = info
-
                 },
                 },
                 _logger);
                 _logger);
 
 
             info.Dispose();
             info.Dispose();
         }
         }
 
 
+        /// <inheritdoc />
         public void UpdateDeviceName(string sessionId, string deviceName)
         public void UpdateDeviceName(string sessionId, string deviceName)
         {
         {
             var session = GetSession(sessionId);
             var session = GetSession(sessionId);
@@ -230,7 +247,6 @@ namespace Emby.Server.Implementations.Session
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         /// <returns>SessionInfo.</returns>
         /// <returns>SessionInfo.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
         public SessionInfo LogSessionActivity(
         public SessionInfo LogSessionActivity(
             string appName,
             string appName,
             string appVersion,
             string appVersion,
@@ -268,14 +284,7 @@ namespace Emby.Server.Implementations.Session
 
 
                 if ((activityDate - userLastActivityDate).TotalSeconds > 60)
                 if ((activityDate - userLastActivityDate).TotalSeconds > 60)
                 {
                 {
-                    try
-                    {
-                        _userManager.UpdateUser(user);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError("Error updating user", ex);
-                    }
+                    _userManager.UpdateUser(user);
                 }
                 }
             }
             }
 
 
@@ -292,18 +301,20 @@ namespace Emby.Server.Implementations.Session
             return session;
             return session;
         }
         }
 
 
+        /// <inheritdoc />
         public void CloseIfNeeded(SessionInfo session)
         public void CloseIfNeeded(SessionInfo session)
         {
         {
             if (!session.SessionControllers.Any(i => i.IsSessionActive))
             if (!session.SessionControllers.Any(i => i.IsSessionActive))
             {
             {
                 var key = GetSessionKey(session.Client, session.DeviceId);
                 var key = GetSessionKey(session.Client, session.DeviceId);
 
 
-                _activeConnections.TryRemove(key, out var removed);
+                _activeConnections.TryRemove(key, out _);
 
 
                 OnSessionEnded(session);
                 OnSessionEnded(session);
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void ReportSessionEnded(string sessionId)
         public void ReportSessionEnded(string sessionId)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -313,7 +324,7 @@ namespace Emby.Server.Implementations.Session
             {
             {
                 var key = GetSessionKey(session.Client, session.DeviceId);
                 var key = GetSessionKey(session.Client, session.DeviceId);
 
 
-                _activeConnections.TryRemove(key, out var removed);
+                _activeConnections.TryRemove(key, out _);
 
 
                 OnSessionEnded(session);
                 OnSessionEnded(session);
             }
             }
@@ -344,7 +355,7 @@ namespace Emby.Server.Implementations.Session
                     var runtimeTicks = libraryItem.RunTimeTicks;
                     var runtimeTicks = libraryItem.RunTimeTicks;
 
 
                     MediaSourceInfo mediaSource = null;
                     MediaSourceInfo mediaSource = null;
-                    if (libraryItem is IHasMediaSources hasMediaSources)
+                    if (libraryItem is IHasMediaSources)
                     {
                     {
                         mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
                         mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
 
 
@@ -396,7 +407,6 @@ namespace Emby.Server.Implementations.Session
         /// Removes the now playing item id.
         /// Removes the now playing item id.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
         private void RemoveNowPlayingItem(SessionInfo session)
         private void RemoveNowPlayingItem(SessionInfo session)
         {
         {
             session.NowPlayingItem = null;
             session.NowPlayingItem = null;
@@ -409,9 +419,7 @@ namespace Emby.Server.Implementations.Session
         }
         }
 
 
         private static string GetSessionKey(string appName, string deviceId)
         private static string GetSessionKey(string appName, string deviceId)
-        {
-            return appName + deviceId;
-        }
+            => appName + deviceId;
 
 
         /// <summary>
         /// <summary>
         /// Gets the connection.
         /// Gets the connection.
@@ -431,6 +439,7 @@ namespace Emby.Server.Implementations.Session
             {
             {
                 throw new ArgumentNullException(nameof(deviceId));
                 throw new ArgumentNullException(nameof(deviceId));
             }
             }
+
             var key = GetSessionKey(appName, deviceId);
             var key = GetSessionKey(appName, deviceId);
 
 
             CheckDisposed();
             CheckDisposed();
@@ -503,7 +512,7 @@ namespace Emby.Server.Implementations.Session
         {
         {
             var users = new List<User>();
             var users = new List<User>();
 
 
-            if (!session.UserId.Equals(Guid.Empty))
+            if (session.UserId != Guid.Empty)
             {
             {
                 var user = _userManager.GetUserById(session.UserId);
                 var user = _userManager.GetUserById(session.UserId);
 
 
@@ -522,8 +531,6 @@ namespace Emby.Server.Implementations.Session
             return users;
             return users;
         }
         }
 
 
-        private Timer _idleTimer;
-
         private void StartIdleCheckTimer()
         private void StartIdleCheckTimer()
         {
         {
             if (_idleTimer == null)
             if (_idleTimer == null)
@@ -599,11 +606,11 @@ namespace Emby.Server.Implementations.Session
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Used to report that playback has started for an item
+        /// Used to report that playback has started for an item.
         /// </summary>
         /// </summary>
         /// <param name="info">The info.</param>
         /// <param name="info">The info.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">info</exception>
+        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
         public async Task OnPlaybackStart(PlaybackStartInfo info)
         public async Task OnPlaybackStart(PlaybackStartInfo info)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -615,7 +622,7 @@ namespace Emby.Server.Implementations.Session
 
 
             var session = GetSession(info.SessionId);
             var session = GetSession(info.SessionId);
 
 
-            var libraryItem = info.ItemId.Equals(Guid.Empty)
+            var libraryItem = info.ItemId == Guid.Empty
                 ? null
                 ? null
                 : GetNowPlayingItem(session, info.ItemId);
                 : GetNowPlayingItem(session, info.ItemId);
 
 
@@ -653,7 +660,6 @@ namespace Emby.Server.Implementations.Session
                     ClientName = session.Client,
                     ClientName = session.Client,
                     DeviceId = session.DeviceId,
                     DeviceId = session.DeviceId,
                     Session = session
                     Session = session
-
                 },
                 },
                 _logger);
                 _logger);
 
 
@@ -684,6 +690,7 @@ namespace Emby.Server.Implementations.Session
             _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
             _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
         }
         }
 
 
+        /// <inheritdoc />
         public Task OnPlaybackProgress(PlaybackProgressInfo info)
         public Task OnPlaybackProgress(PlaybackProgressInfo info)
         {
         {
             return OnPlaybackProgress(info, false);
             return OnPlaybackProgress(info, false);
@@ -857,7 +864,7 @@ namespace Emby.Server.Implementations.Session
                 {
                 {
                     MediaSourceInfo mediaSource = null;
                     MediaSourceInfo mediaSource = null;
 
 
-                    if (libraryItem is IHasMediaSources hasMediaSources)
+                    if (libraryItem is IHasMediaSources)
                     {
                     {
                         mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
                         mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
                     }
                     }
@@ -966,13 +973,17 @@ namespace Emby.Server.Implementations.Session
         /// <param name="sessionId">The session identifier.</param>
         /// <param name="sessionId">The session identifier.</param>
         /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
         /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
         /// <returns>SessionInfo.</returns>
         /// <returns>SessionInfo.</returns>
-        /// <exception cref="ResourceNotFoundException">sessionId</exception>
+        /// <exception cref="ResourceNotFoundException">
+        /// No session with an Id equal to <c>sessionId</c> was found
+        /// and <c>throwOnMissing</c> is <c>true</c>.
+        /// </exception>
         private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
         private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
         {
         {
             var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
             var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
             if (session == null && throwOnMissing)
             if (session == null && throwOnMissing)
             {
             {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
+                throw new ResourceNotFoundException(
+                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
             }
             }
 
 
             return session;
             return session;
@@ -985,12 +996,14 @@ namespace Emby.Server.Implementations.Session
 
 
             if (session == null)
             if (session == null)
             {
             {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
+                throw new ResourceNotFoundException(
+                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
             }
             }
 
 
             return session;
             return session;
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
         public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1011,6 +1024,7 @@ namespace Emby.Server.Implementations.Session
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
         public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1055,6 +1069,7 @@ namespace Emby.Server.Implementations.Session
             return Task.WhenAll(GetTasks());
             return Task.WhenAll(GetTasks());
         }
         }
 
 
+        /// <inheritdoc />
         public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
         public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1096,7 +1111,8 @@ namespace Emby.Server.Implementations.Session
             {
             {
                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
                 {
                 {
-                    throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name));
+                    throw new ArgumentException(
+                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
                 }
                 }
             }
             }
 
 
@@ -1204,6 +1220,7 @@ namespace Emby.Server.Implementations.Session
             return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
             return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
         public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
         {
         {
             var generalCommand = new GeneralCommand
             var generalCommand = new GeneralCommand
@@ -1220,6 +1237,7 @@ namespace Emby.Server.Implementations.Session
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
         public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1303,12 +1321,12 @@ namespace Emby.Server.Implementations.Session
 
 
             var session = GetSession(sessionId);
             var session = GetSession(sessionId);
 
 
-            if (session.UserId.Equals(userId))
+            if (session.UserId == userId)
             {
             {
                 throw new ArgumentException("The requested user is already the primary user of the session.");
                 throw new ArgumentException("The requested user is already the primary user of the session.");
             }
             }
 
 
-            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
+            if (session.AdditionalUsers.All(i => i.UserId != userId))
             {
             {
                 var user = _userManager.GetUserById(userId);
                 var user = _userManager.GetUserById(userId);
 
 
@@ -1430,12 +1448,13 @@ namespace Emby.Server.Implementations.Session
 
 
         private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
         private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
         {
         {
-            var existing = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                DeviceId = deviceId,
-                UserId = user.Id,
-                Limit = 1
-            }).Items.FirstOrDefault();
+            var existing = _authRepo.Get(
+                new AuthenticationInfoQuery
+                {
+                    DeviceId = deviceId,
+                    UserId = user.Id,
+                    Limit = 1
+                }).Items.FirstOrDefault();
 
 
             var allExistingForDevice = _authRepo.Get(
             var allExistingForDevice = _authRepo.Get(
                 new AuthenticationInfoQuery
                 new AuthenticationInfoQuery
@@ -1460,7 +1479,7 @@ namespace Emby.Server.Implementations.Session
 
 
             if (existing != null)
             if (existing != null)
             {
             {
-                _logger.LogInformation("Reissuing access token: " + existing.AccessToken);
+                _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
                 return existing.AccessToken;
                 return existing.AccessToken;
             }
             }
 
 
@@ -1485,6 +1504,7 @@ namespace Emby.Server.Implementations.Session
             return newToken.AccessToken;
             return newToken.AccessToken;
         }
         }
 
 
+        /// <inheritdoc />
         public void Logout(string accessToken)
         public void Logout(string accessToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1494,18 +1514,20 @@ namespace Emby.Server.Implementations.Session
                 throw new ArgumentNullException(nameof(accessToken));
                 throw new ArgumentNullException(nameof(accessToken));
             }
             }
 
 
-            var existing = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                Limit = 1,
-                AccessToken = accessToken
-            }).Items.FirstOrDefault();
+            var existing = _authRepo.Get(
+                new AuthenticationInfoQuery
+                {
+                    Limit = 1,
+                    AccessToken = accessToken
+                }).Items;
 
 
-            if (existing != null)
+            if (existing.Count > 0)
             {
             {
-                Logout(existing);
+                Logout(existing[0]);
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void Logout(AuthenticationInfo existing)
         public void Logout(AuthenticationInfo existing)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1531,6 +1553,7 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void RevokeUserTokens(Guid userId, string currentAccessToken)
         public void RevokeUserTokens(Guid userId, string currentAccessToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1549,6 +1572,7 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void RevokeToken(string token)
         public void RevokeToken(string token)
         {
         {
             Logout(token);
             Logout(token);
@@ -1605,8 +1629,6 @@ namespace Emby.Server.Implementations.Session
             _deviceManager.SaveCapabilities(deviceId, capabilities);
             _deviceManager.SaveCapabilities(deviceId, capabilities);
         }
         }
 
 
-        private DtoOptions _itemInfoDtoOptions;
-
         /// <summary>
         /// <summary>
         /// Converts a BaseItem to a BaseItemInfo.
         /// Converts a BaseItem to a BaseItemInfo.
         /// </summary>
         /// </summary>
@@ -1683,6 +1705,7 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void ReportNowViewingItem(string sessionId, string itemId)
         public void ReportNowViewingItem(string sessionId, string itemId)
         {
         {
             if (string.IsNullOrEmpty(itemId))
             if (string.IsNullOrEmpty(itemId))
@@ -1697,6 +1720,7 @@ namespace Emby.Server.Implementations.Session
             ReportNowViewingItem(sessionId, info);
             ReportNowViewingItem(sessionId, info);
         }
         }
 
 
+        /// <inheritdoc />
         public void ReportNowViewingItem(string sessionId, BaseItemDto item)
         public void ReportNowViewingItem(string sessionId, BaseItemDto item)
         {
         {
             var session = GetSession(sessionId);
             var session = GetSession(sessionId);
@@ -1704,6 +1728,7 @@ namespace Emby.Server.Implementations.Session
             session.NowViewingItem = item;
             session.NowViewingItem = item;
         }
         }
 
 
+        /// <inheritdoc />
         public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
         public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
         {
         {
             var session = Sessions.FirstOrDefault(i =>
             var session = Sessions.FirstOrDefault(i =>
@@ -1715,11 +1740,13 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void ClearTranscodingInfo(string deviceId)
         public void ClearTranscodingInfo(string deviceId)
         {
         {
             ReportTranscodingInfo(deviceId, null);
             ReportTranscodingInfo(deviceId, null);
         }
         }
 
 
+        /// <inheritdoc />
         public SessionInfo GetSession(string deviceId, string client, string version)
         public SessionInfo GetSession(string deviceId, string client, string version)
         {
         {
             return Sessions.FirstOrDefault(i =>
             return Sessions.FirstOrDefault(i =>
@@ -1727,6 +1754,7 @@ namespace Emby.Server.Implementations.Session
                     && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
                     && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
+        /// <inheritdoc />
         public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
         public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
         {
         {
             if (info == null)
             if (info == null)
@@ -1759,23 +1787,24 @@ namespace Emby.Server.Implementations.Session
             return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
             return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
         }
         }
 
 
+        /// <inheritdoc />
         public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
         public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
         {
         {
-            var result = _authRepo.Get(new AuthenticationInfoQuery
+            var items = _authRepo.Get(new AuthenticationInfoQuery
             {
             {
-                AccessToken = token
-            });
-
-            var info = result.Items.FirstOrDefault();
+                AccessToken = token,
+                Limit = 1
+            }).Items;
 
 
-            if (info == null)
+            if (items.Count == 0)
             {
             {
                 return null;
                 return null;
             }
             }
 
 
-            return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null);
+            return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
         public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1785,6 +1814,7 @@ namespace Emby.Server.Implementations.Session
             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
         public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1796,11 +1826,10 @@ namespace Emby.Server.Implementations.Session
                 return Task.CompletedTask;
                 return Task.CompletedTask;
             }
             }
 
 
-            var data = dataFn();
-
-            return SendMessageToSessions(sessions, name, data, cancellationToken);
+            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
         public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1809,6 +1838,7 @@ namespace Emby.Server.Implementations.Session
             return SendMessageToSessions(sessions, name, data, cancellationToken);
             return SendMessageToSessions(sessions, name, data, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
         public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
         {
         {
             CheckDisposed();
             CheckDisposed();
@@ -1817,22 +1847,5 @@ namespace Emby.Server.Implementations.Session
 
 
             return SendMessageToSessions(sessions, name, data, cancellationToken);
             return SendMessageToSessions(sessions, name, data, cancellationToken);
         }
         }
-
-        public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
-        {
-            CheckDisposed();
-
-            var sessions = Sessions
-                .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i));
-
-            return SendMessageToSessions(sessions, name, data, cancellationToken);
-        }
-
-        private bool IsAdminSession(SessionInfo s)
-        {
-            var user = _userManager.GetUserById(s.UserId);
-
-            return user != null && user.Policy.IsAdministrator;
-        }
     }
     }
 }
 }

+ 9 - 11
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Globalization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using SkiaSharp;
 using SkiaSharp;
 using static Jellyfin.Drawing.Skia.SkiaHelper;
 using static Jellyfin.Drawing.Skia.SkiaHelper;
@@ -18,27 +17,23 @@ namespace Jellyfin.Drawing.Skia
     /// </summary>
     /// </summary>
     public class SkiaEncoder : IImageEncoder
     public class SkiaEncoder : IImageEncoder
     {
     {
-        private readonly ILogger _logger;
-        private readonly IApplicationPaths _appPaths;
-        private readonly ILocalizationManager _localizationManager;
-
         private static readonly HashSet<string> _transparentImageTypes
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
 
 
+        private readonly ILogger _logger;
+        private readonly IApplicationPaths _appPaths;
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
         /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">The application logger.</param>
         /// <param name="logger">The application logger.</param>
         /// <param name="appPaths">The application paths.</param>
         /// <param name="appPaths">The application paths.</param>
-        /// <param name="localizationManager">The application localization manager.</param>
         public SkiaEncoder(
         public SkiaEncoder(
             ILogger<SkiaEncoder> logger,
             ILogger<SkiaEncoder> logger,
-            IApplicationPaths appPaths,
-            ILocalizationManager localizationManager)
+            IApplicationPaths appPaths)
         {
         {
             _logger = logger;
             _logger = logger;
             _appPaths = appPaths;
             _appPaths = appPaths;
-            _localizationManager = localizationManager;
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -235,9 +230,12 @@ namespace Jellyfin.Drawing.Skia
 
 
         private bool RequiresSpecialCharacterHack(string path)
         private bool RequiresSpecialCharacterHack(string path)
         {
         {
-            if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
+            for (int i = 0; i < path.Length; i++)
             {
             {
-                return true;
+                if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
+                {
+                    return true;
+                }
             }
             }
 
 
             if (HasDiacritics(path))
             if (HasDiacritics(path))

+ 3 - 8
Jellyfin.Server/Program.cs

@@ -168,7 +168,7 @@ namespace Jellyfin.Server
                 _loggerFactory,
                 _loggerFactory,
                 options,
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NullImageEncoder(),
+                GetImageEncoder(appPaths),
                 new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
                 new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
                 appConfig);
                 appConfig);
             try
             try
@@ -192,8 +192,6 @@ namespace Jellyfin.Server
                     throw;
                     throw;
                 }
                 }
 
 
-                appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager);
-
                 await appHost.RunStartupTasksAsync().ConfigureAwait(false);
                 await appHost.RunStartupTasksAsync().ConfigureAwait(false);
 
 
                 stopWatch.Stop();
                 stopWatch.Stop();
@@ -491,9 +489,7 @@ namespace Jellyfin.Server
             }
             }
         }
         }
 
 
-        private static IImageEncoder GetImageEncoder(
-            IApplicationPaths appPaths,
-            ILocalizationManager localizationManager)
+        private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths)
         {
         {
             try
             try
             {
             {
@@ -502,8 +498,7 @@ namespace Jellyfin.Server
 
 
                 return new SkiaEncoder(
                 return new SkiaEncoder(
                     _loggerFactory.CreateLogger<SkiaEncoder>(),
                     _loggerFactory.CreateLogger<SkiaEncoder>(),
-                    appPaths,
-                    localizationManager);
+                    appPaths);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {

+ 5 - 21
MediaBrowser.Api/Images/ImageService.cs

@@ -24,7 +24,7 @@ using Microsoft.Net.Http.Headers;
 namespace MediaBrowser.Api.Images
 namespace MediaBrowser.Api.Images
 {
 {
     /// <summary>
     /// <summary>
-    /// Class GetItemImage
+    /// Class GetItemImage.
     /// </summary>
     /// </summary>
     [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
     [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
     [Authenticated]
     [Authenticated]
@@ -558,21 +558,6 @@ namespace MediaBrowser.Api.Images
                 throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", displayText, request.Type));
                 throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", displayText, request.Type));
             }
             }
 
 
-            IImageEnhancer[] supportedImageEnhancers;
-            if (_imageProcessor.ImageEnhancers.Count > 0)
-            {
-                if (item == null)
-                {
-                    item = _libraryManager.GetItemById(itemId);
-                }
-
-                supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>();
-            }
-            else
-            {
-                supportedImageEnhancers = Array.Empty<IImageEnhancer>();
-            }
-
             bool cropwhitespace;
             bool cropwhitespace;
             if (request.CropWhitespace.HasValue)
             if (request.CropWhitespace.HasValue)
             {
             {
@@ -598,25 +583,25 @@ namespace MediaBrowser.Api.Images
                 {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
                 {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
             };
             };
 
 
-            return GetImageResult(item,
+            return GetImageResult(
+                item,
                 itemId,
                 itemId,
                 request,
                 request,
                 imageInfo,
                 imageInfo,
                 cropwhitespace,
                 cropwhitespace,
                 outputFormats,
                 outputFormats,
-                supportedImageEnhancers,
                 cacheDuration,
                 cacheDuration,
                 responseHeaders,
                 responseHeaders,
                 isHeadRequest);
                 isHeadRequest);
         }
         }
 
 
-        private async Task<object> GetImageResult(BaseItem item,
+        private async Task<object> GetImageResult(
+            BaseItem item,
             Guid itemId,
             Guid itemId,
             ImageRequest request,
             ImageRequest request,
             ItemImageInfo image,
             ItemImageInfo image,
             bool cropwhitespace,
             bool cropwhitespace,
             IReadOnlyCollection<ImageFormat> supportedFormats,
             IReadOnlyCollection<ImageFormat> supportedFormats,
-            IReadOnlyCollection<IImageEnhancer> enhancers,
             TimeSpan? cacheDuration,
             TimeSpan? cacheDuration,
             IDictionary<string, string> headers,
             IDictionary<string, string> headers,
             bool isHeadRequest)
             bool isHeadRequest)
@@ -624,7 +609,6 @@ namespace MediaBrowser.Api.Images
             var options = new ImageProcessingOptions
             var options = new ImageProcessingOptions
             {
             {
                 CropWhiteSpace = cropwhitespace,
                 CropWhiteSpace = cropwhitespace,
-                Enhancers = enhancers,
                 Height = request.Height,
                 Height = request.Height,
                 ImageIndex = request.Index ?? 0,
                 ImageIndex = request.Index ?? 0,
                 Image = image,
                 Image = image,

+ 2 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -250,11 +250,11 @@ namespace MediaBrowser.Api.Playback
             {
             {
                 if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    logFilePrefix = "ffmpeg-directstream";
+                    logFilePrefix = "ffmpeg-remux";
                 }
                 }
                 else
                 else
                 {
                 {
-                    logFilePrefix = "ffmpeg-remux";
+                    logFilePrefix = "ffmpeg-directstream";
                 }
                 }
             }
             }
 
 

+ 0 - 35
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
@@ -20,20 +19,12 @@ namespace MediaBrowser.Controller.Drawing
         /// <value>The supported input formats.</value>
         /// <value>The supported input formats.</value>
         IReadOnlyCollection<string> SupportedInputFormats { get; }
         IReadOnlyCollection<string> SupportedInputFormats { get; }
 
 
-        /// <summary>
-        /// Gets the image enhancers.
-        /// </summary>
-        /// <value>The image enhancers.</value>
-        IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether [supports image collage creation].
         /// Gets a value indicating whether [supports image collage creation].
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
         bool SupportsImageCollageCreation { get; }
         bool SupportsImageCollageCreation { get; }
 
 
-        IImageEncoder ImageEncoder { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets the dimensions of the image.
         /// Gets the dimensions of the image.
         /// </summary>
         /// </summary>
@@ -58,14 +49,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>ImageDimensions</returns>
         /// <returns>ImageDimensions</returns>
         ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem);
         ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem);
 
 
-        /// <summary>
-        /// Gets the supported enhancers.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <returns>IEnumerable{IImageEnhancer}.</returns>
-        IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType);
-
         /// <summary>
         /// <summary>
         /// Gets the image cache tag.
         /// Gets the image cache tag.
         /// </summary>
         /// </summary>
@@ -75,15 +58,6 @@ namespace MediaBrowser.Controller.Drawing
         string GetImageCacheTag(BaseItem item, ItemImageInfo image);
         string GetImageCacheTag(BaseItem item, ItemImageInfo image);
         string GetImageCacheTag(BaseItem item, ChapterInfo info);
         string GetImageCacheTag(BaseItem item, ChapterInfo info);
 
 
-        /// <summary>
-        /// Gets the image cache tag.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="image">The image.</param>
-        /// <param name="imageEnhancers">The image enhancers.</param>
-        /// <returns>Guid.</returns>
-        string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers);
-
         /// <summary>
         /// <summary>
         /// Processes the image.
         /// Processes the image.
         /// </summary>
         /// </summary>
@@ -99,15 +73,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
         Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
 
 
-        /// <summary>
-        /// Gets the enhanced image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="imageIndex">Index of the image.</param>
-        /// <returns>Task{System.String}.</returns>
-        Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex);
-
         /// <summary>
         /// <summary>
         /// Gets the supported image output formats.
         /// Gets the supported image output formats.
         /// </summary>
         /// </summary>

+ 0 - 2
MediaBrowser.Controller/Drawing/ImageHelper.cs

@@ -19,8 +19,6 @@ namespace MediaBrowser.Controller.Drawing
             return GetSizeEstimate(options);
             return GetSizeEstimate(options);
         }
         }
 
 
-        public static IImageProcessor ImageProcessor { get; set; }
-
         private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
         private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
         {
         {
             if (options.Width.HasValue && options.Height.HasValue)
             if (options.Width.HasValue && options.Height.HasValue)

+ 0 - 3
MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 
 
 namespace MediaBrowser.Controller.Drawing
 namespace MediaBrowser.Controller.Drawing
@@ -34,8 +33,6 @@ namespace MediaBrowser.Controller.Drawing
 
 
         public int Quality { get; set; }
         public int Quality { get; set; }
 
 
-        public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; }
-
         public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
         public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
 
 
         public bool AddPlayedIndicator { get; set; }
         public bool AddPlayedIndicator { get; set; }

+ 3 - 5
MediaBrowser.Controller/Entities/Photo.cs

@@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.Entities
         public override double GetDefaultPrimaryImageAspectRatio()
         public override double GetDefaultPrimaryImageAspectRatio()
         {
         {
             // REVIEW: @bond
             // REVIEW: @bond
-            if (Width.HasValue && Height.HasValue)
+            if (Width != 0 && Height != 0)
             {
             {
-                double width = Width.Value;
-                double height = Height.Value;
+                double width = Width;
+                double height = Height;
 
 
                 if (Orientation.HasValue)
                 if (Orientation.HasValue)
                 {
                 {
@@ -67,8 +67,6 @@ namespace MediaBrowser.Controller.Entities
             return base.GetDefaultPrimaryImageAspectRatio();
             return base.GetDefaultPrimaryImageAspectRatio();
         }
         }
 
 
-        public new int? Width { get; set; }
-        public new int? Height { get; set; }
         public string CameraMake { get; set; }
         public string CameraMake { get; set; }
         public string CameraModel { get; set; }
         public string CameraModel { get; set; }
         public string Software { get; set; }
         public string Software { get; set; }

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

@@ -157,7 +157,8 @@ namespace MediaBrowser.Controller.Library
         /// <param name="introProviders">The intro providers.</param>
         /// <param name="introProviders">The intro providers.</param>
         /// <param name="itemComparers">The item comparers.</param>
         /// <param name="itemComparers">The item comparers.</param>
         /// <param name="postscanTasks">The postscan tasks.</param>
         /// <param name="postscanTasks">The postscan tasks.</param>
-        void AddParts(IEnumerable<IResolverIgnoreRule> rules,
+        void AddParts(
+            IEnumerable<IResolverIgnoreRule> rules,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IIntroProvider> introProviders,
             IEnumerable<IIntroProvider> introProviders,
             IEnumerable<IBaseItemComparer> itemComparers,
             IEnumerable<IBaseItemComparer> itemComparers,
@@ -349,9 +350,6 @@ namespace MediaBrowser.Controller.Library
         /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
         bool IsAudioFile(string path);
         bool IsAudioFile(string path);
 
 
-        bool IsAudioFile(string path, LibraryOptions libraryOptions);
-        bool IsVideoFile(string path, LibraryOptions libraryOptions);
-
         /// <summary>
         /// <summary>
         /// Gets the season number from path.
         /// Gets the season number from path.
         /// </summary>
         /// </summary>

+ 3 - 0
MediaBrowser.Controller/Library/IMediaSourceProvider.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;

+ 2 - 10
MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs

@@ -316,11 +316,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
             {
                 if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
                 if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
                 {
                 {
-                    var size = new ImageDimensions
-                    {
-                        Width = VideoStream.Width.Value,
-                        Height = VideoStream.Height.Value
-                    };
+                    var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
 
 
                     var newSize = DrawingUtils.Resize(size,
                     var newSize = DrawingUtils.Resize(size,
                         BaseRequest.Width ?? 0,
                         BaseRequest.Width ?? 0,
@@ -346,11 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
             {
                 if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
                 if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
                 {
                 {
-                    var size = new ImageDimensions
-                    {
-                        Width = VideoStream.Width.Value,
-                        Height = VideoStream.Height.Value
-                    };
+                    var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
 
 
                     var newSize = DrawingUtils.Resize(size,
                     var newSize = DrawingUtils.Resize(size,
                         BaseRequest.Width ?? 0,
                         BaseRequest.Width ?? 0,

+ 0 - 61
MediaBrowser.Controller/Providers/IImageEnhancer.cs

@@ -1,61 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Providers
-{
-    public interface IImageEnhancer
-    {
-        /// <summary>
-        /// Return true only if the given image for the given item will be enhanced by this enhancer.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
-        bool Supports(BaseItem item, ImageType imageType);
-
-        /// <summary>
-        /// Gets the priority or order in which this enhancer should be run.
-        /// </summary>
-        /// <value>The priority.</value>
-        MetadataProviderPriority Priority { get; }
-
-        /// <summary>
-        /// Return a key incorporating all configuration information related to this item
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <returns>Cache key relating to the current state of this item and configuration</returns>
-        string GetConfigurationCacheKey(BaseItem item, ImageType imageType);
-
-        /// <summary>
-        /// Gets the size of the enhanced image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="imageIndex">Index of the image.</param>
-        /// <param name="originalImageSize">Size of the original image.</param>
-        /// <returns>ImageSize.</returns>
-        ImageDimensions GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageDimensions originalImageSize);
-
-        EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex);
-
-        /// <summary>
-        /// Enhances the image async.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="inputFile">The input file.</param>
-        /// <param name="outputFile">The output file.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="imageIndex">Index of the image.</param>
-        /// <returns>Task{Image}.</returns>
-        /// <exception cref="System.ArgumentNullException"></exception>
-        Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex);
-    }
-
-    public class EnhancedImageInfo
-    {
-        public bool RequiresTransparency { get; set; }
-    }
-}

+ 19 - 12
MediaBrowser.Model/Drawing/ImageDimensions.cs

@@ -1,39 +1,46 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 #pragma warning disable SA1600
 #pragma warning disable SA1600
 
 
+using System.Globalization;
+
 namespace MediaBrowser.Model.Drawing
 namespace MediaBrowser.Model.Drawing
 {
 {
     /// <summary>
     /// <summary>
     /// Struct ImageDimensions.
     /// Struct ImageDimensions.
     /// </summary>
     /// </summary>
-    public struct ImageDimensions
+    public readonly struct ImageDimensions
     {
     {
+        public ImageDimensions(int width, int height)
+        {
+            Width = width;
+            Height = height;
+        }
+
         /// <summary>
         /// <summary>
-        /// Gets or sets the height.
+        /// Gets the height.
         /// </summary>
         /// </summary>
         /// <value>The height.</value>
         /// <value>The height.</value>
-        public int Height { get; set; }
+        public int Height { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the width.
+        /// Gets the width.
         /// </summary>
         /// </summary>
         /// <value>The width.</value>
         /// <value>The width.</value>
-        public int Width { get; set; }
+        public int Width { get; }
 
 
         public bool Equals(ImageDimensions size)
         public bool Equals(ImageDimensions size)
         {
         {
             return Width.Equals(size.Width) && Height.Equals(size.Height);
             return Width.Equals(size.Width) && Height.Equals(size.Height);
         }
         }
 
 
+        /// <inheritdoc />
         public override string ToString()
         public override string ToString()
         {
         {
-            return string.Format("{0}-{1}", Width, Height);
-        }
-
-        public ImageDimensions(int width, int height)
-        {
-            Width = width;
-            Height = height;
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0}-{1}",
+                Width,
+                Height);
         }
         }
     }
     }
 }
 }

+ 6 - 0
MediaBrowser.Model/Querying/QueryResult.cs

@@ -30,5 +30,11 @@ namespace MediaBrowser.Model.Querying
         {
         {
             Items = Array.Empty<T>();
             Items = Array.Empty<T>();
         }
         }
+
+        public QueryResult(IReadOnlyList<T> items)
+        {
+            Items = items;
+            TotalRecordCount = items.Count;
+        }
     }
     }
 }
 }

+ 2 - 0
jellyfin.ruleset

@@ -5,6 +5,8 @@
     <Rule Id="SA1202" Action="Info" />
     <Rule Id="SA1202" Action="Info" />
     <!-- disable warning SA1204: Static members must appear before non-static members -->
     <!-- disable warning SA1204: Static members must appear before non-static members -->
     <Rule Id="SA1204" Action="Info" />
     <Rule Id="SA1204" Action="Info" />
+    <!-- disable warning SA1404: Code analysis suppression should have justification -->
+    <Rule Id="SA1404" Action="Info" />
 
 
     <!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
     <!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
     <Rule Id="SA1009" Action="None" />
     <Rule Id="SA1009" Action="None" />

+ 70 - 380
tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs

@@ -6,388 +6,78 @@ namespace Jellyfin.Naming.Tests.TV
 {
 {
     public class EpisodeNumberTests
     public class EpisodeNumberTests
     {
     {
-        [Fact]
-        public void TestEpisodeNumber1()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/S02E03 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber40()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber41()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber42()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber43()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber44()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber45()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber46()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber47()
-        {
-            Assert.Equal(36, GetEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber52()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode - 16.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber53()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber54()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16 - Some Title.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber57()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 Some Title.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber58()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 - 12 Some Title.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber59()
-        {
-            Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 - 12 Angry Men.avi"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber60()
-        {
-            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 12 Some Title.avi"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber61()
-        {
-            Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 12 Angry Men.avi"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber62()
-        {
-            // This is not supported. Expected to fail, although it would be a good one to add support for.
-            Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.E03.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber63()
-        {
-            Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.S04E03.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber64()
-        {
-            Assert.Equal(368, GetEpisodeNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber65()
-        {
-            // Not supported yet
-            Assert.Equal(7, GetEpisodeNumberFromFile(@"/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber30()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber31()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber32()
-        {
-            Assert.Equal(9, GetEpisodeNumberFromFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber33()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber34()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber35()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber36()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber37()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber38()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber39()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber20()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber21()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber22()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber23()
-        {
-            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber24()
-        {
-            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber25()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber26()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber27()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
-        }
-
-        // FIXME
-        // [Fact]
-        public void TestEpisodeNumber28()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber29()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber11()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber12()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber13()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber14()
-        {
-            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber15()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber16()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber17()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber18()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber19()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber2()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber3()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber4()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber5()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber6()
-        {
-            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber7()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber8()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber9()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber10()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber48()
-        {
-            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/2. Infestation.avi"));
-        }
-
-        [Fact]
-        public void TestEpisodeNumber49()
-        {
-            Assert.Equal(7, GetEpisodeNumberFromFile(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi"));
-        }
-
-        private int? GetEpisodeNumberFromFile(string path)
-        {
-            var options = new NamingOptions();
-
-            var result = new EpisodePathParser(options)
+        private readonly NamingOptions _namingOptions = new NamingOptions();
+
+        [Theory]
+        [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", 3)]
+        [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 22)]
+        [InlineData("Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", 1)]
+        [InlineData("After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", 6)]
+        [InlineData("Season 02/S02E03 blah.avi", 3)]
+        [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 1/01x02 blah.avi", 2)]
+        [InlineData("Season 1/S01x02 blah.avi", 2)]
+        [InlineData("Season 1/S01E02 blah.avi", 2)]
+        [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 3)]
+        [InlineData("Season 1/S01xE02 blah.avi", 2)]
+        [InlineData("Season 1/seriesname S01E02 blah.avi", 2)]
+        [InlineData("Season 2/Episode - 16.avi", 16)]
+        [InlineData("Season 2/Episode 16.avi", 16)]
+        [InlineData("Season 2/Episode 16 - Some Title.avi", 16)]
+        [InlineData("Season 2/16 Some Title.avi", 16)]
+        [InlineData("Season 2/16 - 12 Some Title.avi", 16)]
+        [InlineData("Season 2/7 - 12 Angry Men.avi", 7)]
+        [InlineData("Season 1/seriesname 01x02 blah.avi", 2)]
+        [InlineData("Season 25/The Simpsons.S25E09.Steal this episode.mp4", 9)]
+        [InlineData("Season 1/seriesname S01x02 blah.avi", 2)]
+        [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 1/seriesname S01xE02 blah.avi", 2)]
+        [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 3)]
+        [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 3)]
+        [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 3)]
+        [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 23)]
+        [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 23)]
+        [InlineData("Season 2009/2009x02 blah.avi", 2)]
+        [InlineData("Season 2009/S2009x02 blah.avi", 2)]
+        [InlineData("Season 2009/S2009E02 blah.avi", 2)]
+        [InlineData("Season 2009/seriesname 2009x02 blah.avi", 2)]
+        [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/S2009xE02 blah.avi", 2)]
+        [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 23)]
+        [InlineData("Season 2009/seriesname S2009xE02 blah.avi", 2)]
+        [InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/seriesname S2009E02 blah.avi", 2)]
+        [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/seriesname S2009x02 blah.avi", 2)]
+        [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 3)]
+        [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 3)]
+        [InlineData("Season 1/02 - blah-02 a.avi", 2)]
+        [InlineData("Season 1/02 - blah.avi", 2)]
+        [InlineData("Season 2/02 - blah 14 blah.avi", 2)]
+        [InlineData("Season 2/02.avi", 2)]
+        [InlineData("Season 2/2. Infestation.avi", 2)]
+        [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", 7)]
+        [InlineData("Running Man/Running Man S2017E368.mkv", 368)]
+        // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
+        // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
+        // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
+        // TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
+        // TODO: [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)]
+        // TODO: [InlineData("Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv", 136)]
+        public void GetEpisodeNumberFromFileTest(string path, int? expected)
+        {
+            var result = new EpisodePathParser(_namingOptions)
                 .Parse(path, false);
                 .Parse(path, false);
 
 
-            return result.EpisodeNumber;
+            Assert.Equal(expected, result.EpisodeNumber);
         }
         }
     }
     }
 }
 }

+ 13 - 3
tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs

@@ -6,11 +6,21 @@ namespace Jellyfin.Naming.Tests.TV
 {
 {
     public class SeasonNumberTests
     public class SeasonNumberTests
     {
     {
-        private int? GetSeasonNumberFromEpisodeFile(string path)
+        private readonly NamingOptions _namingOptions = new NamingOptions();
+
+        [Theory]
+        [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 25)]
+        public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
         {
         {
-            var options = new NamingOptions();
+            var result = new EpisodeResolver(_namingOptions)
+                .Resolve(path, false);
 
 
-            var result = new EpisodeResolver(options)
+            Assert.Equal(expected, result.SeasonNumber);
+        }
+
+        private int? GetSeasonNumberFromEpisodeFile(string path)
+        {
+            var result = new EpisodeResolver(_namingOptions)
                 .Resolve(path, false);
                 .Resolve(path, false);
 
 
             return result.SeasonNumber;
             return result.SeasonNumber;