Browse Source

Merge pull request #21 from jellyfin/master

nightly
artiume 5 years ago
parent
commit
697aee5b0c
80 changed files with 825 additions and 1385 deletions
  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" />
   </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>

+ 34 - 314
Emby.Drawing/ImageProcessor.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
@@ -11,7 +10,6 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -23,7 +21,7 @@ namespace Emby.Drawing
     /// <summary>
     /// Class ImageProcessor.
     /// </summary>
-    public class ImageProcessor : IImageProcessor, IDisposable
+    public sealed class ImageProcessor : IImageProcessor, IDisposable
     {
         // Increment this when there's a change requiring caches to be invalidated
         private const string Version = "3";
@@ -31,28 +29,24 @@ namespace Emby.Drawing
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
 
-        /// <summary>
-        /// The _logger
-        /// </summary>
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
-        private IImageEncoder _imageEncoder;
+        private readonly IImageEncoder _imageEncoder;
         private readonly Func<ILibraryManager> _libraryManager;
         private readonly Func<IMediaEncoder> _mediaEncoder;
 
-        private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
         private bool _disposed = false;
 
         /// <summary>
-        ///
+        /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
         /// </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(
             ILogger<ImageProcessor> logger,
             IServerApplicationPaths appPaths,
@@ -67,16 +61,10 @@ namespace Emby.Drawing
             _libraryManager = libraryManager;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
-
-            ImageEnhancers = Array.Empty<IImageEnhancer>();
-
-            ImageHelper.ImageProcessor = this;
         }
 
         private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
 
-        private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
-
         /// <inheritdoc />
         public IReadOnlyCollection<string> SupportedInputFormats =>
             new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@@ -89,9 +77,7 @@ namespace Emby.Drawing
                 "aiff",
                 "cr2",
                 "crw",
-
-                // Remove until supported
-                //"nef",
+                "nef",
                 "orf",
                 "pef",
                 "arw",
@@ -110,19 +96,9 @@ namespace Emby.Drawing
                 "wbmp"
             };
 
-        /// <inheritdoc />
-        public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
-
         /// <inheritdoc />
         public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
 
-        /// <inheritdoc />
-        public IImageEncoder ImageEncoder
-        {
-            get => _imageEncoder;
-            set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
-        }
-
         /// <inheritdoc />
         public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
         {
@@ -150,6 +126,8 @@ namespace Emby.Drawing
                 throw new ArgumentNullException(nameof(options));
             }
 
+            var libraryManager = _libraryManager();
+
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
 
@@ -157,9 +135,10 @@ namespace Emby.Drawing
             {
                 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;
@@ -186,27 +165,6 @@ namespace Emby.Drawing
             dateModified = supportedImageInfo.dateModified;
             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;
             ImageOrientation? orientation = null;
             if (item is Photo photo)
@@ -239,12 +197,6 @@ namespace Emby.Drawing
             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);
 
-            CheckDisposed();
-
-            LockInfo lockInfo = GetLock(cacheFilePath);
-
-            await lockInfo.Lock.WaitAsync().ConfigureAwait(false);
-
             try
             {
                 if (!File.Exists(cacheFilePath))
@@ -270,10 +222,6 @@ namespace Emby.Drawing
                 _logger.LogError(ex, "Error encoding image");
                 return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
-            finally
-            {
-                ReleaseLock(cacheFilePath, lockInfo);
-            }
         }
 
         private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
@@ -305,20 +253,18 @@ namespace Emby.Drawing
         }
 
         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>
-        /// Gets the cache file path based on a set of parameters
+        /// Gets the cache file path based on a set of parameters.
         /// </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)
         {
@@ -400,11 +346,7 @@ namespace Emby.Drawing
 
         /// <inheritdoc />
         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 />
         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)
         {
             var inputFormat = Path.GetExtension(originalImagePath)
@@ -487,154 +409,6 @@ namespace Emby.Drawing
             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>
         /// Gets the cache path.
         /// </summary>
@@ -647,7 +421,7 @@ namespace Emby.Drawing
         /// or
         /// uniqueName
         /// or
-        /// fileExtension
+        /// fileExtension.
         /// </exception>
         public string GetCachePath(string path, string uniqueName, string fileExtension)
         {
@@ -680,7 +454,7 @@ namespace Emby.Drawing
         /// <exception cref="ArgumentNullException">
         /// path
         /// or
-        /// filename
+        /// filename.
         /// </exception>
         public string GetCachePath(string path, string filename)
         {
@@ -688,6 +462,7 @@ namespace Emby.Drawing
             {
                 throw new ArgumentNullException(nameof(path));
             }
+
             if (string.IsNullOrEmpty(filename))
             {
                 throw new ArgumentNullException(nameof(filename));
@@ -709,74 +484,19 @@ namespace Emby.Drawing
         }
 
         /// <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();
             }
-        }
 
-        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
 {
-    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;
-            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,
                 // so we make sure this one gets tested first.
                 // "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
                 },

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

@@ -4,7 +4,6 @@
 using System;
 using System.Globalization;
 using System.IO;
-using System.Linq;
 
 namespace Emby.Naming.TV
 {
@@ -29,14 +28,14 @@ namespace Emby.Naming.TV
         {
             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)
             {
                 result.Success = true;
-                result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder;
+                result.IsSeasonFolder = isSeasonFolder;
             }
 
             return result;
@@ -90,12 +89,10 @@ namespace Emby.Naming.TV
             // Look for one of the season folder names
             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));
-                    if (result.Item1.HasValue)
+                    if (result.seasonNumber.HasValue)
                     {
                         return result;
                     }
@@ -105,25 +102,32 @@ namespace Emby.Naming.TV
             }
 
             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))
             {
-                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>
@@ -131,7 +135,7 @@ namespace Emby.Naming.TV
         /// </summary>
         /// <param name="path">The path.</param>
         /// <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 length = 0;
@@ -142,7 +146,7 @@ namespace Emby.Naming.TV
             // Find out where the numbers start, and then keep going until they end
             for (var i = 0; i < path.Length; i++)
             {
-                if (char.IsNumber(path, i))
+                if (char.IsNumber(path[i]))
                 {
                     if (!hasOpenParenth)
                     {
@@ -177,7 +181,7 @@ namespace Emby.Naming.TV
                 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 (!new AudioFileParser(_options).IsAudioFile(path))
+                if (!AudioFileParser.IsAudioFile(path, _options))
                 {
                     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
             var input = !file.IsDirectory
@@ -204,7 +204,7 @@ namespace Emby.Naming.Video
             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);
 

+ 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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -16,7 +22,7 @@ namespace Emby.Notifications.Api
     public class GetNotifications : IReturn<NotificationResult>
     {
         [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")]
         public bool? IsRead { get; set; }
@@ -30,32 +36,34 @@ namespace Emby.Notifications.Api
 
     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 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 class NotificationResult
     {
-        public Notification[] Notifications { get; set; }
+        public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
+
         public int TotalRecordCount { get; set; }
     }
 
     public class NotificationsSummary
     {
         public int UnreadCount { get; set; }
+
         public NotificationLevel MaxUnreadNotificationLevel { get; set; }
     }
 
@@ -63,7 +71,7 @@ namespace Emby.Notifications.Api
     public class GetNotificationsSummary : IReturn<NotificationsSummary>
     {
         [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")]
@@ -80,16 +88,16 @@ namespace Emby.Notifications.Api
     public class AddAdminNotification : IReturnVoid
     {
         [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")]
-        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")]
-        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")]
-        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")]
         public NotificationLevel Level { get; set; }
@@ -99,20 +107,20 @@ namespace Emby.Notifications.Api
     public class MarkRead : IReturnVoid
     {
         [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)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
 
     [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
     public class MarkUnread : IReturnVoid
     {
         [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)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
 
     [Authenticated]
@@ -127,32 +135,29 @@ namespace Emby.Notifications.Api
             _userManager = userManager;
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationTypes request)
         {
             return _notificationManager.GetNotificationTypes();
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationServices request)
         {
             return _notificationManager.GetNotificationServices().ToList();
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationsSummary request)
         {
             return new NotificationsSummary
             {
-
             };
         }
 
         public Task Post(AddAdminNotification request)
         {
             // This endpoint really just exists as post of a real with sickbeard
-            return AddNotification(request);
-        }
-
-        private Task AddNotification(AddAdminNotification request)
-        {
             var notification = new NotificationRequest
             {
                 Date = DateTime.UtcNow,
@@ -166,14 +171,17 @@ namespace Emby.Notifications.Api
             return _notificationManager.SendNotification(notification, CancellationToken.None);
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkRead request)
         {
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkUnread request)
         {
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotifications request)
         {
             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.Collections.Generic;
 using System.Linq;

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

@@ -4,6 +4,8 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <ItemGroup>
@@ -16,4 +18,16 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </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>

+ 4 - 1
Emby.Notifications/NotificationConfigurationFactory.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Notifications;
@@ -13,7 +16,7 @@ namespace Emby.Notifications
                 new ConfigurationStore
                 {
                     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
 {
     /// <summary>
-    /// Creates notifications for various system events
+    /// Creates notifications for various system events.
     /// </summary>
-    public class Notifications : IServerEntryPoint
+    public class NotificationEntryPoint : IServerEntryPoint
     {
         private readonly ILogger _logger;
-
+        private readonly IActivityManager _activityManager;
+        private readonly ILocalizationManager _localization;
         private readonly INotificationManager _notificationManager;
-
         private readonly ILibraryManager _libraryManager;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfigurationManager _config;
 
-        private Timer LibraryUpdateTimer { get; set; }
         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;
 
-        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,
             ILocalizationManager localization,
-            ILogger logger,
             INotificationManager notificationManager,
             ILibraryManager libraryManager,
             IServerApplicationHost appHost,
             IConfigurationManager config)
         {
             _logger = logger;
+            _activityManager = activityManager;
+            _localization = localization;
             _notificationManager = notificationManager;
             _libraryManager = libraryManager;
             _appHost = appHost;
             _config = config;
-            _localization = localization;
-            _activityManager = activityManager;
 
             _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
         }
 
+        /// <inheritdoc />
         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;
         }
 
-        private async void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
         {
             var type = NotificationType.ServerRestartRequired.ToString();
 
             var notification = new NotificationRequest
             {
                 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);
         }
 
-        private async void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
         {
             var entry = e.Argument;
 
@@ -117,7 +132,7 @@ namespace Emby.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)
             {
@@ -136,8 +151,7 @@ namespace Emby.Notifications
             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))
             {
@@ -146,14 +160,17 @@ namespace Emby.Notifications
 
             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
                 {
-                    LibraryUpdateTimer.Change(5000, Timeout.Infinite);
+                    _libraryUpdateTimer.Change(5000, Timeout.Infinite);
                 }
 
                 _itemsAdded.Add(e.Item);
@@ -188,7 +205,8 @@ namespace Emby.Notifications
             {
                 items = _itemsAdded.ToList();
                 _itemsAdded.Clear();
-                DisposeLibraryUpdateTimer();
+                _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
+                _libraryUpdateTimer = null;
             }
 
             items = items.Take(10).ToList();
@@ -198,7 +216,10 @@ namespace Emby.Notifications
                 var notification = new NotificationRequest
                 {
                     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
                 };
 
@@ -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)
         {
             var name = item.Name;
@@ -219,6 +245,7 @@ namespace Emby.Notifications
                         episode.IndexNumber.Value,
                         name);
                 }
+
                 if (episode.ParentIndexNumber.HasValue)
                 {
                     name = string.Format(
@@ -229,7 +256,6 @@ namespace Emby.Notifications
                 }
             }
 
-
             if (item is IHasSeries hasSeries)
             {
                 name = hasSeries.SeriesName + " - " + name;
@@ -257,7 +283,7 @@ namespace Emby.Notifications
             return name;
         }
 
-        private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem)
+        private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
         {
             try
             {
@@ -269,23 +295,37 @@ namespace Emby.Notifications
             }
         }
 
+        /// <inheritdoc />
         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
 {
+    /// <summary>
+    /// NotificationManager class.
+    /// </summary>
     public class NotificationManager : INotificationManager
     {
         private readonly ILogger _logger;
         private readonly IUserManager _userManager;
         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;
             _config = config;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
         }
 
         private NotificationOptions GetConfiguration()
@@ -37,12 +49,14 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
 
+        /// <inheritdoc />
         public Task SendNotification(NotificationRequest request, CancellationToken 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;
 
@@ -64,7 +78,8 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
         }
 
-        private Task SendNotification(NotificationRequest request,
+        private Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             IEnumerable<User> users,
             string title,
@@ -79,7 +94,7 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
         }
 
-        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption options)
+        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
         {
             if (request.SendToUserMode.HasValue)
             {
@@ -109,7 +124,8 @@ namespace Emby.Notifications
             return request.UserIds;
         }
 
-        private async Task SendNotification(NotificationRequest request,
+        private async Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             string title,
             string description,
@@ -161,12 +177,14 @@ namespace Emby.Notifications
             return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
         }
 
+        /// <inheritdoc />
         public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
         {
             _services = services.ToArray();
             _typeFactories = notificationTypeFactories.ToArray();
         }
 
+        /// <inheritdoc />
         public List<NotificationTypeInfo> GetNotificationTypes()
         {
             var list = _typeFactories.Select(i =>
@@ -180,7 +198,6 @@ namespace Emby.Notifications
                     _logger.LogError(ex, "Error in GetNotificationTypes");
                     return new List<NotificationTypeInfo>();
                 }
-
             }).SelectMany(i => i).ToList();
 
             var config = GetConfiguration();
@@ -193,13 +210,13 @@ namespace Emby.Notifications
             return list;
         }
 
+        /// <inheritdoc />
         public IEnumerable<NameIdPair> GetNotificationServices()
         {
             return _services.Select(i => new NameIdPair
             {
                 Name = i.Name,
                 Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
-
             }).OrderBy(i => i.Name);
         }
     }

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

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

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

@@ -29,7 +29,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Activity
 {
-    public class ActivityLogEntryPoint : IServerEntryPoint
+    public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
         private readonly ILogger _logger;
         private readonly IInstallationManager _installationManager;
@@ -39,7 +39,6 @@ namespace Emby.Server.Implementations.Activity
         private readonly ILocalizationManager _localization;
         private readonly ISubtitleManager _subManager;
         private readonly IUserManager _userManager;
-        private readonly IServerApplicationHost _appHost;
         private readonly IDeviceManager _deviceManager;
 
         /// <summary>
@@ -64,8 +63,7 @@ namespace Emby.Server.Implementations.Activity
             ILocalizationManager localization,
             IInstallationManager installationManager,
             ISubtitleManager subManager,
-            IUserManager userManager,
-            IServerApplicationHost appHost)
+            IUserManager userManager)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -76,7 +74,6 @@ namespace Emby.Server.Implementations.Activity
             _installationManager = installationManager;
             _subManager = subManager;
             _userManager = userManager;
-            _appHost = appHost;
         }
 
         public Task RunAsync()
@@ -141,7 +138,7 @@ namespace Emby.Server.Implementations.Activity
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     e.Provider,
-                    Notifications.Notifications.GetItemName(e.Item)),
+                    Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
                 Type = "SubtitleDownloadFailure",
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ShortOverview = e.Exception.Message
@@ -533,6 +530,7 @@ namespace Emby.Server.Implementations.Activity
         private void CreateLogEntry(ActivityLogEntry entry)
             => _activityManager.Create(entry);
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _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);
             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<IDlnaManager>(
@@ -836,7 +847,10 @@ namespace Emby.Server.Implementations
             UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
             serviceCollection.AddSingleton(UserViewManager);
 
-            NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager);
+            NotificationManager = new NotificationManager(
+                LoggerFactory.CreateLogger<NotificationManager>(),
+                UserManager,
+                ServerConfigurationManager);
             serviceCollection.AddSingleton(NotificationManager);
 
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
@@ -1074,8 +1088,6 @@ namespace Emby.Server.Implementations
                 GetExports<IMetadataSaver>(),
                 GetExports<IExternalId>());
 
-            ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>();
-
             LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
             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();
-            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
             if (includeTypes.Length == 0)
             {
@@ -3553,6 +3539,19 @@ namespace Emby.Server.Implementations.Data
                     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)
             {
@@ -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
         readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
 
-        private IEnumerable<string> MapIncludeItemTypes(string value)
+        private string[] MapIncludeItemTypes(string value)
         {
             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;
         }
 
-        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)
             {
-                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)
             {
-                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
 
-            list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i)));
+            list.AddRange(inheritedTags.Select(i => (6, i)));
 
             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))
             {
@@ -5658,7 +5657,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             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 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)
         {
-            var sessions = _authRepo.Get(new AuthenticationInfoQuery
+            IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
             {
                 //UserId = query.UserId
                 HasUser = true
-
             }).Items;
 
             // 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;
 
-                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))
             {
                 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();
 
-            return new QueryResult<DeviceInfo>
-            {
-                Items = array,
-                TotalRecordCount = array.Length
-            };
+            return new QueryResult<DeviceInfo>(array);
         }
 
         private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
@@ -186,7 +181,7 @@ namespace Emby.Server.Implementations.Devices
                 LastUserName = authInfo.UserName,
                 Name = authInfo.DeviceName,
                 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;
             }
 
-            var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray();
-
             ImageDimensions size;
 
             var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
 
             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;
                 }
             }
-
-            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;

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

@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
-    public class RecordingNotifier : IServerEntryPoint
+    public sealed class RecordingNotifier : IServerEntryPoint
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ISessionManager _sessionManager;
@@ -28,32 +28,33 @@ namespace Emby.Server.Implementations.EntryPoints
             _liveTvManager = liveTvManager;
         }
 
+        /// <inheritdoc />
         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;
         }
 
-        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);
         }
 
-        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);
         }
 
-        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);
         }
 
-        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);
         }
@@ -64,11 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             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)
             {
@@ -76,12 +73,13 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
+        /// <inheritdoc />
         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.Model.IO;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
@@ -15,21 +14,17 @@ namespace Emby.Server.Implementations.EntryPoints
     /// </summary>
     public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
     {
-        private readonly ILogger _logger;
-
         /// <summary>
         /// The user manager.
         /// </summary>
         private readonly IUserManager _userManager;
-
-        private IFileSystem _fileSystem;
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
         /// </summary>
-        public RefreshUsersMetadata(ILogger logger, IUserManager userManager, IFileSystem fileSystem)
+        public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
         {
-            _logger = logger;
             _userManager = userManager;
             _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.Configuration;
 using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
     /// <summary>
     /// Class StartupWizard.
     /// </summary>
-    public class StartupWizard : IServerEntryPoint
+    public sealed class StartupWizard : IServerEntryPoint
     {
         /// <summary>
         /// The app host.
         /// </summary>
         private readonly IServerApplicationHost _appHost;
-
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly ILogger _logger;
-
-        private IServerConfigurationManager _config;
+        private readonly IServerConfigurationManager _config;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StartupWizard"/> class.
         /// </summary>
         /// <param name="appHost">The application host.</param>
-        /// <param name="logger">The logger.</param>
         /// <param name="config">The configuration manager.</param>
-        public StartupWizard(IServerApplicationHost appHost, ILogger logger, IServerConfigurationManager config)
+        public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config)
         {
             _appHost = appHost;
-            _logger = logger;
             _config = config;
         }
 

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

@@ -3,8 +3,6 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -23,9 +21,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// The logger.
         /// </summary>
         private readonly ILogger _logger;
-        private readonly ISocketFactory _socketFactory;
         private readonly IServerApplicationHost _appHost;
-        private readonly IJsonSerializer _json;
 
         /// <summary>
         /// The UDP server.
@@ -64,7 +60,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             _cancellationTokenSource.Cancel();
             _udpServer.Dispose();
-
+            _cancellationTokenSource.Dispose();
             _cancellationTokenSource = 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.Model.Entities;
 using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
 
 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 ILogger _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserManager _userManager;
 
+        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
+
         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;
             _sessionManager = sessionManager;
-            _logger = logger;
             _userManager = userManager;
         }
 
         public Task RunAsync()
         {
-            _userDataManager.UserDataSaved += _userDataManager_UserDataSaved;
+            _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
 
             return Task.CompletedTask;
         }
 
-        void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e)
+        void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
         {
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
             {
@@ -54,14 +53,17 @@ namespace Emby.Server.Implementations.EntryPoints
 
             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
                 {
-                    UpdateTimer.Change(UpdateDuration, Timeout.Infinite);
+                    _updateTimer.Change(UpdateDuration, Timeout.Infinite);
                 }
 
                 if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
@@ -97,10 +99,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 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()
         {
-            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))
             {
                 _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);

+ 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 string _defaultRedirectPath;
         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 IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
         public HttpListenerHost(
@@ -72,6 +72,8 @@ namespace Emby.Server.Implementations.HttpServer
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
         }
 
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
         public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
 
         public static HttpListenerHost Instance { get; protected set; }
@@ -82,8 +84,6 @@ namespace Emby.Server.Implementations.HttpServer
 
         public ServiceController ServiceController { get; private set; }
 
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
         public object CreateInstance(Type type)
         {
             return _appHost.CreateInstance(type);
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.HttpServer
 
         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
                 return path;
@@ -131,13 +131,13 @@ namespace Emby.Server.Implementations.HttpServer
 
         public Type GetServiceTypeByRequest(Type requestType)
         {
-            ServiceOperationsMap.TryGetValue(requestType, out var serviceType);
+            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
             return serviceType;
         }
 
         public void AddServiceInfo(Type serviceType, Type requestType)
         {
-            ServiceOperationsMap[requestType] = serviceType;
+            _serviceOperationsMap[requestType] = serviceType;
         }
 
         private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
@@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.HttpServer
                 else
                 {
                     var inners = agg.InnerExceptions;
-                    if (inners != null && inners.Count > 0)
+                    if (inners.Count > 0)
                     {
                         return GetActualException(inners[0]);
                     }
@@ -362,7 +362,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return true;
             }
 
-            host = host ?? string.Empty;
+            host ??= string.Empty;
 
             if (_networkManager.IsInPrivateAddressSpace(host))
             {
@@ -433,7 +433,7 @@ namespace Emby.Server.Implementations.HttpServer
         }
 
         /// <summary>
-        /// Overridable method that can be used to implement a custom hnandler
+        /// Overridable method that can be used to implement a custom handler.
         /// </summary>
         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, "/", StringComparison.OrdinalIgnoreCase)
                     || 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
                     _logger.LogDebug("Normalizing a URL at {0}", localPath);
@@ -693,7 +693,10 @@ namespace Emby.Server.Implementations.HttpServer
 
         protected virtual void Dispose(bool disposing)
         {
-            if (_disposed) return;
+            if (_disposed)
+            {
+                return;
+            }
 
             if (disposing)
             {

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

@@ -6,7 +6,9 @@ namespace Emby.Server.Implementations.IO
     public class ExtendedFileSystemInfo
     {
         public bool IsHidden { get; set; }
+
         public bool IsReadOnly { 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
     {
-        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 Timer _timer;
         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)
         {
             logger.LogDebug("New file refresher created for {0}", path);
             Path = path;
 
-            ConfigurationManager = configurationManager;
-            LibraryManager = libraryManager;
-            Logger = logger;
+            _configurationManager = configurationManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
             AddPath(path);
         }
 
+        public event EventHandler<EventArgs> Completed;
+
+        public string Path { get; private set; }
+
         private void AddAffectedPath(string path)
         {
             if (string.IsNullOrEmpty(path))
@@ -80,11 +82,11 @@ namespace Emby.Server.Implementations.IO
 
                 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
                 {
-                    _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)
             {
-                Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
+                _logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
 
                 Path = path;
                 AddAffectedPath(path);
@@ -116,7 +118,7 @@ namespace Emby.Server.Implementations.IO
                 paths = _affectedPaths.ToList();
             }
 
-            Logger.LogDebug("Timer stopped.");
+            _logger.LogDebug("Timer stopped.");
 
             DisposeTimer();
             Completed?.Invoke(this, EventArgs.Empty);
@@ -127,7 +129,7 @@ namespace Emby.Server.Implementations.IO
             }
             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;
                 }
 
-                Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
 
                 try
                 {
@@ -158,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // 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)
                 {
-                    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))
             {
-                item = LibraryManager.FindByPath(path, null);
+                item = _libraryManager.FindByPath(path, null);
 
                 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)
                 .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
-                .OrderBy(i => i.Name)
                 .ToList();
         }
 
@@ -1406,25 +1405,32 @@ namespace Emby.Server.Implementations.Library
 
         private void SetTopParentOrAncestorIds(InternalItemsQuery query)
         {
-            if (query.AncestorIds.Length == 0)
+            var ancestorIds = query.AncestorIds;
+            int len = ancestorIds.Length;
+            if (len == 0)
             {
                 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)
@@ -1585,7 +1591,7 @@ namespace Emby.Server.Implementations.Library
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         {
             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)
                 .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);
         }
 
-        public bool IsVideoFile(string path, LibraryOptions libraryOptions)
+        /// <inheritdoc />
+        public bool IsVideoFile(string path)
         {
             var resolver = new VideoResolver(GetNamingOptions());
             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)
-        {
-            return IsAudioFile(path, new LibraryOptions());
-        }
+            => AudioFileParser.IsAudioFile(path, GetNamingOptions());
 
+        /// <inheritdoc />
         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)
         {
             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
 
-            var libraryOptions = args.GetLibraryOptions();
             var collectionType = args.GetCollectionType();
 
             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);
             }
 
-            if (LibraryManager.IsAudioFile(args.Path, libraryOptions))
+            if (LibraryManager.IsAudioFile(args.Path))
             {
                 var extension = Path.GetExtension(args.Path);
 
@@ -105,7 +104,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
 
                 // For conflicting extensions, give priority to videos
-                if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions))
+                if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
                 {
                     return null;
                 }
@@ -121,7 +120,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 {
                     item = new MediaBrowser.Controller.Entities.Audio.Audio();
                 }
-
                 else if (isBooksCollectionType)
                 {
                     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.Providers;
 using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
@@ -78,9 +77,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// Determine if the supplied file data points to a music album.
         /// </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>
@@ -94,7 +93,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             if (args.IsDirectory)
             {
                 // 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;
                 }
@@ -112,7 +111,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             IDirectoryService directoryService,
             ILogger logger,
             IFileSystem fileSystem,
-            LibraryOptions libraryOptions,
             ILibraryManager libraryManager)
         {
             var discSubfolderCount = 0;
@@ -132,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                         }
 
                         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)
                         {
@@ -153,7 +151,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 {
                     var fullName = fileSystemInfo.FullName;
 
-                    if (libraryManager.IsAudioFile(fullName, libraryOptions))
+                    if (libraryManager.IsAudioFile(fullName))
                     {
                         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
-            if (args.Parent.IsRoot) return null;
+            if (args.Parent.IsRoot)
+            {
+                return null;
+            }
 
             var directoryService = args.DirectoryService;
 
             var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
             // 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;
                         }
+
                         if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
                         {
                             videoInfo = parser.ResolveDirectory(args.Path);
@@ -137,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return null;
                 }
 
-                if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub)
+                if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
                 {
                     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)
             {
                 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)
                 {
@@ -446,8 +446,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return movie;
                 }
             }
-
-            if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
+            else if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
             {
                 return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
             }
@@ -519,14 +518,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 return null;
             }
 
+            int additionalPartsLen = folderPaths.Count - 1;
+            var additionalParts = new string[additionalPartsLen];
+            folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen);
+
             var returnVideo = new T
             {
                 Path = folderPaths[0],
-
-                AdditionalParts = folderPaths.Skip(1).ToArray(),
-
+                AdditionalParts = additionalParts,
                 VideoType = videoTypes[0],
-
                 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))
                 {
-                    var libraryOptions = args.GetLibraryOptions();
                     var filename = file.Name;
                     var ownedByMedia = false;
 
                     foreach (var siblingFile in files)
                     {
-                        if (PhotoResolver.IsOwnedByMedia(_libraryManager, libraryOptions, siblingFile.FullName, filename))
+                        if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
                         {
                             ownedByMedia = true;
                             break;

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

@@ -8,7 +8,6 @@ using System.Linq;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 
 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
                         var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
-                        var libraryOptions = args.GetLibraryOptions();
 
                         foreach (var file in files)
                         {
-                            if (IsOwnedByMedia(_libraryManager, libraryOptions, file.FullName, filename))
+                            if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
                             {
                                 return null;
                             }
@@ -78,17 +76,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
             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;
         }
 
-        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);
 
         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;
                     }
 
-                    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
                         {
@@ -123,24 +123,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
             IFileSystem fileSystem,
             ILogger logger,
             ILibraryManager libraryManager,
-            LibraryOptions libraryOptions,
             bool isTvContentType)
         {
             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 (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
@@ -152,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 else
                 {
                     string fullName = child.FullName;
-                    if (libraryManager.IsVideoFile(fullName, libraryOptions))
+                    if (libraryManager.IsVideoFile(fullName))
                     {
                         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.IO;
 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.Entities;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 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.Collections.Generic;
 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 MediaBrowser.Controller.Plugins;
 
@@ -5,11 +8,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
     public class EntryPoint : IServerEntryPoint
     {
+        /// <inheritdoc />
         public Task RunAsync()
         {
             return EmbyTV.Current.Start();
         }
 
+        /// <inheritdoc />
         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.Threading;
 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.Collections.Generic;
 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.Globalization;
 using MediaBrowser.Controller.LiveTv;
@@ -21,7 +24,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 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;
                 }
                 else if (info.OriginalAirDate.HasValue)
@@ -32,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     else
                     {
-                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd");
+                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
                     }
                 }
                 else
@@ -67,14 +74,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             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.Month.ToString("00", CultureInfo.InvariantCulture),
                 date.Day.ToString("00", CultureInfo.InvariantCulture),
                 date.Hour.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 MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Serialization;
@@ -12,6 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         }
 
+        /// <inheritdoc />
         public override void Add(SeriesTimerInfo item)
         {
             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.Collections.Concurrent;
 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.Collections.Concurrent;
 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.Collections.Generic;
 using System.Globalization;
@@ -91,12 +94,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 {
                     using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
                     {
-                        await gzStream.CopyToAsync(fileStream).ConfigureAwait(false);
+                        await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
                     }
                 }
                 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 MediaBrowser.Common.Configuration;
 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.Globalization;
 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 SA1600
 
 using System;
 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.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv
 {
     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 IJsonSerializer _jsonSerializer;
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IMediaEncoder _mediaEncoder;
         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;
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _mediaSourceManager = mediaSourceManager;
-            _mediaEncoder = mediaEncoder;
             _appHost = appHost;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _appPaths = appPaths;
         }
 
         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);
 
-                if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null)
+                if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null)
                 {
                     return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
                 }
@@ -55,10 +51,6 @@ namespace Emby.Server.Implementations.LiveTv
             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)
         {
             IEnumerable<MediaSourceInfo> sources;
@@ -91,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv
             foreach (var source in list)
             {
                 source.Type = MediaSourceType.Default;
-                source.BufferMs = source.BufferMs ?? 1500;
+                source.BufferMs ??= 1500;
 
                 if (source.RequiresOpening || forceRequireOpening)
                 {
@@ -100,11 +92,14 @@ namespace Emby.Server.Implementations.LiveTv
 
                 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
@@ -114,11 +109,12 @@ namespace Emby.Server.Implementations.LiveTv
                 }
             }
 
-            _logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+            _logger.LogDebug("MediaSources: {@MediaSources}", list);
 
             return list;
         }
 
+        /// <inheritdoc />
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
             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.Collections.Concurrent;
 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.Collections.Generic;
 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.Buffers;
 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.Collections.Generic;
 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.Collections.Generic;
 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.Collections.Generic;
 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.Collections.Generic;
 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.Collections.Generic;
 using System.IO;

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

@@ -3,7 +3,7 @@
     "ItemRemovedWithName": "{0} var fjarlægt úr safninu",
     "ItemAddedWithName": "{0} var bætt í safnið",
     "Inherit": "Erfa",
-    "HomeVideos": "Myndbönd að heiman",
+    "HomeVideos": "Heimamyndbönd",
     "HeaderRecordingGroups": "Upptökuhópar",
     "HeaderNextUp": "Næst á dagskrá",
     "HeaderLiveTV": "Sjónvarp í beinni útsendingu",
@@ -36,10 +36,10 @@
     "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
     "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
     "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",
     "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
-    "NotificationOptionPluginInstalled": "Viðbót settur upp",
+    "NotificationOptionPluginInstalled": "Viðbót sett upp",
     "NotificationOptionPluginError": "Bilun í viðbót",
     "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
     "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
@@ -50,15 +50,15 @@
     "NameSeasonUnknown": "Sería óþekkt",
     "NameSeasonNumber": "Sería {0}",
     "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",
-    "LabelRunningTimeValue": "Keyrslutími kerfis: {0}",
+    "LabelRunningTimeValue": "spilunartími: {0}",
     "User": "Notandi",
     "System": "Kerfi",
     "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",
     "MusicVideos": "Tónlistarmyndbönd",
     "Music": "Tónlist",
@@ -74,5 +74,23 @@
     "PluginUpdatedWithName": "{0} var uppfært",
     "PluginUninstalledWithName": "{0} var fjarlægt",
     "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 =
             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;
 
+        /// <inheritdoc />
         public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
 
         /// <summary>
@@ -81,40 +116,23 @@ namespace Emby.Server.Implementations.Session
         /// </summary>
         public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> SessionStarted;
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 
+        /// <inheritdoc />
         public event EventHandler<SessionEventArgs> SessionEnded;
 
+        /// <inheritdoc />
         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)
         {
@@ -135,14 +153,17 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
-        private bool _disposed = false;
-
+        /// <inheritdoc />
         public void Dispose()
         {
             Dispose(true);
             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)
         {
             if (_disposed)
@@ -152,15 +173,17 @@ namespace Emby.Server.Implementations.Session
 
             if (disposing)
             {
-                // TODO: dispose stuff
+                _idleTimer?.Dispose();
             }
 
+            _idleTimer = null;
+
             _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 
             _disposed = true;
         }
 
-        public void CheckDisposed()
+        private void CheckDisposed()
         {
             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)
         {
             if (!string.IsNullOrEmpty(info.DeviceId))
@@ -204,13 +221,13 @@ namespace Emby.Server.Implementations.Session
                 new SessionEventArgs
                 {
                     SessionInfo = info
-
                 },
                 _logger);
 
             info.Dispose();
         }
 
+        /// <inheritdoc />
         public void UpdateDeviceName(string sessionId, string deviceName)
         {
             var session = GetSession(sessionId);
@@ -230,7 +247,6 @@ namespace Emby.Server.Implementations.Session
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <param name="user">The user.</param>
         /// <returns>SessionInfo.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
         public SessionInfo LogSessionActivity(
             string appName,
             string appVersion,
@@ -268,14 +284,7 @@ namespace Emby.Server.Implementations.Session
 
                 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;
         }
 
+        /// <inheritdoc />
         public void CloseIfNeeded(SessionInfo session)
         {
             if (!session.SessionControllers.Any(i => i.IsSessionActive))
             {
                 var key = GetSessionKey(session.Client, session.DeviceId);
 
-                _activeConnections.TryRemove(key, out var removed);
+                _activeConnections.TryRemove(key, out _);
 
                 OnSessionEnded(session);
             }
         }
 
+        /// <inheritdoc />
         public void ReportSessionEnded(string sessionId)
         {
             CheckDisposed();
@@ -313,7 +324,7 @@ namespace Emby.Server.Implementations.Session
             {
                 var key = GetSessionKey(session.Client, session.DeviceId);
 
-                _activeConnections.TryRemove(key, out var removed);
+                _activeConnections.TryRemove(key, out _);
 
                 OnSessionEnded(session);
             }
@@ -344,7 +355,7 @@ namespace Emby.Server.Implementations.Session
                     var runtimeTicks = libraryItem.RunTimeTicks;
 
                     MediaSourceInfo mediaSource = null;
-                    if (libraryItem is IHasMediaSources hasMediaSources)
+                    if (libraryItem is IHasMediaSources)
                     {
                         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.
         /// </summary>
         /// <param name="session">The session.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
         private void RemoveNowPlayingItem(SessionInfo session)
         {
             session.NowPlayingItem = null;
@@ -409,9 +419,7 @@ namespace Emby.Server.Implementations.Session
         }
 
         private static string GetSessionKey(string appName, string deviceId)
-        {
-            return appName + deviceId;
-        }
+            => appName + deviceId;
 
         /// <summary>
         /// Gets the connection.
@@ -431,6 +439,7 @@ namespace Emby.Server.Implementations.Session
             {
                 throw new ArgumentNullException(nameof(deviceId));
             }
+
             var key = GetSessionKey(appName, deviceId);
 
             CheckDisposed();
@@ -503,7 +512,7 @@ namespace Emby.Server.Implementations.Session
         {
             var users = new List<User>();
 
-            if (!session.UserId.Equals(Guid.Empty))
+            if (session.UserId != Guid.Empty)
             {
                 var user = _userManager.GetUserById(session.UserId);
 
@@ -522,8 +531,6 @@ namespace Emby.Server.Implementations.Session
             return users;
         }
 
-        private Timer _idleTimer;
-
         private void StartIdleCheckTimer()
         {
             if (_idleTimer == null)
@@ -599,11 +606,11 @@ namespace Emby.Server.Implementations.Session
         }
 
         /// <summary>
-        /// Used to report that playback has started for an item
+        /// Used to report that playback has started for an item.
         /// </summary>
         /// <param name="info">The info.</param>
         /// <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)
         {
             CheckDisposed();
@@ -615,7 +622,7 @@ namespace Emby.Server.Implementations.Session
 
             var session = GetSession(info.SessionId);
 
-            var libraryItem = info.ItemId.Equals(Guid.Empty)
+            var libraryItem = info.ItemId == Guid.Empty
                 ? null
                 : GetNowPlayingItem(session, info.ItemId);
 
@@ -653,7 +660,6 @@ namespace Emby.Server.Implementations.Session
                     ClientName = session.Client,
                     DeviceId = session.DeviceId,
                     Session = session
-
                 },
                 _logger);
 
@@ -684,6 +690,7 @@ namespace Emby.Server.Implementations.Session
             _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
         }
 
+        /// <inheritdoc />
         public Task OnPlaybackProgress(PlaybackProgressInfo info)
         {
             return OnPlaybackProgress(info, false);
@@ -857,7 +864,7 @@ namespace Emby.Server.Implementations.Session
                 {
                     MediaSourceInfo mediaSource = null;
 
-                    if (libraryItem is IHasMediaSources hasMediaSources)
+                    if (libraryItem is IHasMediaSources)
                     {
                         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="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
         /// <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)
         {
             var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
             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;
@@ -985,12 +996,14 @@ namespace Emby.Server.Implementations.Session
 
             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;
         }
 
+        /// <inheritdoc />
         public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1011,6 +1024,7 @@ namespace Emby.Server.Implementations.Session
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1055,6 +1069,7 @@ namespace Emby.Server.Implementations.Session
             return Task.WhenAll(GetTasks());
         }
 
+        /// <inheritdoc />
         public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1096,7 +1111,8 @@ namespace Emby.Server.Implementations.Session
             {
                 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 });
         }
 
+        /// <inheritdoc />
         public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
         {
             var generalCommand = new GeneralCommand
@@ -1220,6 +1237,7 @@ namespace Emby.Server.Implementations.Session
             return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1303,12 +1321,12 @@ namespace Emby.Server.Implementations.Session
 
             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.");
             }
 
-            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
+            if (session.AdditionalUsers.All(i => i.UserId != 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)
         {
-            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(
                 new AuthenticationInfoQuery
@@ -1460,7 +1479,7 @@ namespace Emby.Server.Implementations.Session
 
             if (existing != null)
             {
-                _logger.LogInformation("Reissuing access token: " + existing.AccessToken);
+                _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
                 return existing.AccessToken;
             }
 
@@ -1485,6 +1504,7 @@ namespace Emby.Server.Implementations.Session
             return newToken.AccessToken;
         }
 
+        /// <inheritdoc />
         public void Logout(string accessToken)
         {
             CheckDisposed();
@@ -1494,18 +1514,20 @@ namespace Emby.Server.Implementations.Session
                 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)
         {
             CheckDisposed();
@@ -1531,6 +1553,7 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
+        /// <inheritdoc />
         public void RevokeUserTokens(Guid userId, string currentAccessToken)
         {
             CheckDisposed();
@@ -1549,6 +1572,7 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
+        /// <inheritdoc />
         public void RevokeToken(string token)
         {
             Logout(token);
@@ -1605,8 +1629,6 @@ namespace Emby.Server.Implementations.Session
             _deviceManager.SaveCapabilities(deviceId, capabilities);
         }
 
-        private DtoOptions _itemInfoDtoOptions;
-
         /// <summary>
         /// Converts a BaseItem to a BaseItemInfo.
         /// </summary>
@@ -1683,6 +1705,7 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
+        /// <inheritdoc />
         public void ReportNowViewingItem(string sessionId, string itemId)
         {
             if (string.IsNullOrEmpty(itemId))
@@ -1697,6 +1720,7 @@ namespace Emby.Server.Implementations.Session
             ReportNowViewingItem(sessionId, info);
         }
 
+        /// <inheritdoc />
         public void ReportNowViewingItem(string sessionId, BaseItemDto item)
         {
             var session = GetSession(sessionId);
@@ -1704,6 +1728,7 @@ namespace Emby.Server.Implementations.Session
             session.NowViewingItem = item;
         }
 
+        /// <inheritdoc />
         public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
         {
             var session = Sessions.FirstOrDefault(i =>
@@ -1715,11 +1740,13 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
+        /// <inheritdoc />
         public void ClearTranscodingInfo(string deviceId)
         {
             ReportTranscodingInfo(deviceId, null);
         }
 
+        /// <inheritdoc />
         public SessionInfo GetSession(string deviceId, string client, string version)
         {
             return Sessions.FirstOrDefault(i =>
@@ -1727,6 +1754,7 @@ namespace Emby.Server.Implementations.Session
                     && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
         }
 
+        /// <inheritdoc />
         public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
         {
             if (info == null)
@@ -1759,23 +1787,24 @@ namespace Emby.Server.Implementations.Session
             return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
         }
 
+        /// <inheritdoc />
         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 GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null);
+            return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
         }
 
+        /// <inheritdoc />
         public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1785,6 +1814,7 @@ namespace Emby.Server.Implementations.Session
             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1796,11 +1826,10 @@ namespace Emby.Server.Implementations.Session
                 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)
         {
             CheckDisposed();
@@ -1809,6 +1838,7 @@ namespace Emby.Server.Implementations.Session
             return SendMessageToSessions(sessions, name, data, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1817,22 +1847,5 @@ namespace Emby.Server.Implementations.Session
 
             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.Extensions;
 using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Globalization;
 using Microsoft.Extensions.Logging;
 using SkiaSharp;
 using static Jellyfin.Drawing.Skia.SkiaHelper;
@@ -18,27 +17,23 @@ namespace Jellyfin.Drawing.Skia
     /// </summary>
     public class SkiaEncoder : IImageEncoder
     {
-        private readonly ILogger _logger;
-        private readonly IApplicationPaths _appPaths;
-        private readonly ILocalizationManager _localizationManager;
-
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
 
+        private readonly ILogger _logger;
+        private readonly IApplicationPaths _appPaths;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
         /// </summary>
         /// <param name="logger">The application logger.</param>
         /// <param name="appPaths">The application paths.</param>
-        /// <param name="localizationManager">The application localization manager.</param>
         public SkiaEncoder(
             ILogger<SkiaEncoder> logger,
-            IApplicationPaths appPaths,
-            ILocalizationManager localizationManager)
+            IApplicationPaths appPaths)
         {
             _logger = logger;
             _appPaths = appPaths;
-            _localizationManager = localizationManager;
         }
 
         /// <inheritdoc/>
@@ -235,9 +230,12 @@ namespace Jellyfin.Drawing.Skia
 
         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))

+ 3 - 8
Jellyfin.Server/Program.cs

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

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

@@ -24,7 +24,7 @@ using Microsoft.Net.Http.Headers;
 namespace MediaBrowser.Api.Images
 {
     /// <summary>
-    /// Class GetItemImage
+    /// Class GetItemImage.
     /// </summary>
     [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
     [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));
             }
 
-            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;
             if (request.CropWhitespace.HasValue)
             {
@@ -598,25 +583,25 @@ namespace MediaBrowser.Api.Images
                 {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
             };
 
-            return GetImageResult(item,
+            return GetImageResult(
+                item,
                 itemId,
                 request,
                 imageInfo,
                 cropwhitespace,
                 outputFormats,
-                supportedImageEnhancers,
                 cacheDuration,
                 responseHeaders,
                 isHeadRequest);
         }
 
-        private async Task<object> GetImageResult(BaseItem item,
+        private async Task<object> GetImageResult(
+            BaseItem item,
             Guid itemId,
             ImageRequest request,
             ItemImageInfo image,
             bool cropwhitespace,
             IReadOnlyCollection<ImageFormat> supportedFormats,
-            IReadOnlyCollection<IImageEnhancer> enhancers,
             TimeSpan? cacheDuration,
             IDictionary<string, string> headers,
             bool isHeadRequest)
@@ -624,7 +609,6 @@ namespace MediaBrowser.Api.Images
             var options = new ImageProcessingOptions
             {
                 CropWhiteSpace = cropwhitespace,
-                Enhancers = enhancers,
                 Height = request.Height,
                 ImageIndex = request.Index ?? 0,
                 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))
                 {
-                    logFilePrefix = "ffmpeg-directstream";
+                    logFilePrefix = "ffmpeg-remux";
                 }
                 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.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
 
@@ -20,20 +19,12 @@ namespace MediaBrowser.Controller.Drawing
         /// <value>The supported input formats.</value>
         IReadOnlyCollection<string> SupportedInputFormats { get; }
 
-        /// <summary>
-        /// Gets the image enhancers.
-        /// </summary>
-        /// <value>The image enhancers.</value>
-        IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
-
         /// <summary>
         /// Gets a value indicating whether [supports image collage creation].
         /// </summary>
         /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
         bool SupportsImageCollageCreation { get; }
 
-        IImageEncoder ImageEncoder { get; set; }
-
         /// <summary>
         /// Gets the dimensions of the image.
         /// </summary>
@@ -58,14 +49,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>ImageDimensions</returns>
         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>
         /// Gets the image cache tag.
         /// </summary>
@@ -75,15 +58,6 @@ namespace MediaBrowser.Controller.Drawing
         string GetImageCacheTag(BaseItem item, ItemImageInfo image);
         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>
         /// Processes the image.
         /// </summary>
@@ -99,15 +73,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>Task.</returns>
         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>
         /// Gets the supported image output formats.
         /// </summary>

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

@@ -19,8 +19,6 @@ namespace MediaBrowser.Controller.Drawing
             return GetSizeEstimate(options);
         }
 
-        public static IImageProcessor ImageProcessor { get; set; }
-
         private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
         {
             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.Linq;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 
 namespace MediaBrowser.Controller.Drawing
@@ -34,8 +33,6 @@ namespace MediaBrowser.Controller.Drawing
 
         public int Quality { get; set; }
 
-        public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; }
-
         public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { 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()
         {
             // 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)
                 {
@@ -67,8 +67,6 @@ namespace MediaBrowser.Controller.Entities
             return base.GetDefaultPrimaryImageAspectRatio();
         }
 
-        public new int? Width { get; set; }
-        public new int? Height { get; set; }
         public string CameraMake { get; set; }
         public string CameraModel { 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="itemComparers">The item comparers.</param>
         /// <param name="postscanTasks">The postscan tasks.</param>
-        void AddParts(IEnumerable<IResolverIgnoreRule> rules,
+        void AddParts(
+            IEnumerable<IResolverIgnoreRule> rules,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IIntroProvider> introProviders,
             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>
         bool IsAudioFile(string path);
 
-        bool IsAudioFile(string path, LibraryOptions libraryOptions);
-        bool IsVideoFile(string path, LibraryOptions libraryOptions);
-
         /// <summary>
         /// Gets the season number from path.
         /// </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.Threading;
 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)
                 {
-                    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,
                         BaseRequest.Width ?? 0,
@@ -346,11 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 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,
                         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 SA1600
 
+using System.Globalization;
+
 namespace MediaBrowser.Model.Drawing
 {
     /// <summary>
     /// Struct ImageDimensions.
     /// </summary>
-    public struct ImageDimensions
+    public readonly struct ImageDimensions
     {
+        public ImageDimensions(int width, int height)
+        {
+            Width = width;
+            Height = height;
+        }
+
         /// <summary>
-        /// Gets or sets the height.
+        /// Gets the height.
         /// </summary>
         /// <value>The height.</value>
-        public int Height { get; set; }
+        public int Height { get; }
 
         /// <summary>
-        /// Gets or sets the width.
+        /// Gets the width.
         /// </summary>
         /// <value>The width.</value>
-        public int Width { get; set; }
+        public int Width { get; }
 
         public bool Equals(ImageDimensions size)
         {
             return Width.Equals(size.Width) && Height.Equals(size.Height);
         }
 
+        /// <inheritdoc />
         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>();
         }
+
+        public QueryResult(IReadOnlyList<T> items)
+        {
+            Items = items;
+            TotalRecordCount = items.Count;
+        }
     }
 }

+ 2 - 0
jellyfin.ruleset

@@ -5,6 +5,8 @@
     <Rule Id="SA1202" Action="Info" />
     <!-- disable warning SA1204: Static members must appear before non-static members -->
     <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. -->
     <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
     {
-        [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);
 
-            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
     {
-        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);
 
             return result.SeasonNumber;