Răsfoiți Sursa

Merge branch 'master' into install-plugin

dkanada 5 ani în urmă
părinte
comite
fe16c3fad4
100 a modificat fișierele cu 2122 adăugiri și 664 ștergeri
  1. 11 1
      .vscode/tasks.json
  2. 1 1
      Dockerfile.arm
  3. 1 1
      DvdLib/Ifo/Program.cs
  4. 2 6
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  5. 5 5
      Emby.Dlna/Main/DlnaEntryPoint.cs
  6. 10 10
      Emby.Dlna/PlayTo/Device.cs
  7. 12 7
      Emby.Dlna/PlayTo/PlayToController.cs
  8. 6 10
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  9. 4 10
      Emby.Dlna/Ssdp/Extensions.cs
  10. 23 2
      Emby.Drawing/ImageProcessor.cs
  11. 6 0
      Emby.Drawing/NullImageEncoder.cs
  12. 1 1
      Emby.Naming/Common/NamingOptions.cs
  13. 4 0
      Emby.Server.Implementations/ApplicationHost.cs
  14. 2 2
      Emby.Server.Implementations/Channels/ChannelManager.cs
  15. 1 1
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  16. 2 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  17. 21 13
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  18. 1 7
      Emby.Server.Implementations/Devices/DeviceManager.cs
  19. 79 15
      Emby.Server.Implementations/Dto/DtoService.cs
  20. 1 0
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  21. 2 2
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  22. 9 9
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  23. 20 20
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  24. 8 4
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  25. 9 4
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  26. 39 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  27. 9 0
      Emby.Server.Implementations/IStartupOptions.cs
  28. 0 38
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  29. 97 5
      Emby.Server.Implementations/Library/LibraryManager.cs
  30. 1 5
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  31. 5 30
      Emby.Server.Implementations/Library/UserManager.cs
  32. 4 9
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  33. 2 2
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  34. 2 2
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  35. 20 59
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  36. 2 0
      Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
  37. 117 0
      Emby.Server.Implementations/Localization/Core/es_419.json
  38. 2 2
      Emby.Server.Implementations/Localization/Core/fr.json
  39. 99 0
      Emby.Server.Implementations/Localization/Core/ta.json
  40. 2 0
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  41. 2 0
      Emby.Server.Implementations/Net/SocketFactory.cs
  42. 2 0
      Emby.Server.Implementations/Net/UdpSocket.cs
  43. 2 0
      Emby.Server.Implementations/Networking/NetworkManager.cs
  44. 2 0
      Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
  45. 3 3
      Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs
  46. 3 4
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  47. 2 0
      Emby.Server.Implementations/ResourceFileManager.cs
  48. 12 10
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  49. 5 10
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  50. 7 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  51. 7 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  52. 9 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  53. 7 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
  54. 4 6
      Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
  55. 2 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  56. 12 5
      Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
  57. 3 4
      Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
  58. 2 0
      Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
  59. 5 1
      Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
  60. 2 0
      Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
  61. 2 0
      Emby.Server.Implementations/Security/AuthenticationRepository.cs
  62. 5 0
      Emby.Server.Implementations/Serialization/JsonSerializer.cs
  63. 2 0
      Emby.Server.Implementations/Services/HttpResult.cs
  64. 3 4
      Emby.Server.Implementations/Services/RequestHelper.cs
  65. 3 2
      Emby.Server.Implementations/Services/ResponseHelper.cs
  66. 2 0
      Emby.Server.Implementations/Services/ServiceController.cs
  67. 2 0
      Emby.Server.Implementations/Services/ServiceExec.cs
  68. 2 0
      Emby.Server.Implementations/Services/ServiceHandler.cs
  69. 2 0
      Emby.Server.Implementations/Services/ServiceMethod.cs
  70. 2 0
      Emby.Server.Implementations/Services/ServicePath.cs
  71. 2 0
      Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
  72. 2 0
      Emby.Server.Implementations/Services/SwaggerService.cs
  73. 2 0
      Emby.Server.Implementations/Services/UrlExtensions.cs
  74. 19 0
      Emby.Server.Implementations/Session/SessionManager.cs
  75. 244 1
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  76. 0 18
      Emby.Server.Implementations/SocketSharp/HttpFile.cs
  77. 0 198
      Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs
  78. 3 1
      Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
  79. 2 0
      Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
  80. 12 6
      Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
  81. 9 9
      Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
  82. 20 18
      Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
  83. 8 6
      Emby.Server.Implementations/Sorting/IsFolderComparer.cs
  84. 20 18
      Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
  85. 20 18
      Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
  86. 8 6
      Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
  87. 8 6
      Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
  88. 10 9
      Emby.Server.Implementations/Sorting/StartDateComparer.cs
  89. 2 0
      Emby.Server.Implementations/Sorting/StudioComparer.cs
  90. 522 0
      Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
  91. 405 0
      Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
  92. 3 4
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  93. 13 3
      Emby.Server.Implementations/Udp/UdpServer.cs
  94. 2 2
      Emby.Server.Implementations/Updates/InstallationManager.cs
  95. 2 0
      Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs
  96. 3 3
      Emby.Server.Implementations/UserViews/DynamicImageProvider.cs
  97. 4 2
      Emby.Server.Implementations/UserViews/FolderImageProvider.cs
  98. 2 0
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  99. 15 0
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  100. 0 1
      Jellyfin.Server/Migrations/IMigrationRoutine.cs

+ 11 - 1
.vscode/tasks.json

@@ -10,6 +10,16 @@
                 "${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
                 "${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
             ],
             ],
             "problemMatcher": "$msCompile"
             "problemMatcher": "$msCompile"
+        },
+        {
+            "label": "api tests",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "test",
+                "${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
+            ],
+            "problemMatcher": "$msCompile"
         }
         }
     ]
     ]
-}
+}

+ 1 - 1
Dockerfile.arm

@@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
  curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
- curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
+ curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
  echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
  echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
  echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
  echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
  apt-get update && \
  apt-get update && \

+ 1 - 1
DvdLib/Ifo/Program.cs

@@ -6,7 +6,7 @@ namespace DvdLib.Ifo
 {
 {
     public class Program
     public class Program
     {
     {
-        public readonly List<Cell> Cells;
+        public IReadOnlyList<Cell> Cells { get; }
 
 
         public Program(List<Cell> cells)
         public Program(List<Cell> cells)
         {
         {

+ 2 - 6
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
 using Emby.Dlna.Service;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
@@ -136,12 +137,7 @@ namespace Emby.Dlna.ContentDirectory
                 }
                 }
             }
             }
 
 
-            foreach (var user in _userManager.Users)
-            {
-                return user;
-            }
-
-            return null;
+            return _userManager.Users.FirstOrDefault();
         }
         }
     }
     }
 }
 }

+ 5 - 5
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -133,20 +133,20 @@ namespace Emby.Dlna.Main
         {
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
 
 
-            ReloadComponents();
+            await ReloadComponents().ConfigureAwait(false);
 
 
-            _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+            _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
         }
         }
 
 
-        void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
         {
         {
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             {
             {
-                ReloadComponents();
+                await ReloadComponents().ConfigureAwait(false);
             }
             }
         }
         }
 
 
-        private async void ReloadComponents()
+        private async Task ReloadComponents()
         {
         {
             var options = _config.GetDlnaConfiguration();
             var options = _config.GetDlnaConfiguration();
 
 

+ 10 - 10
Emby.Dlna/PlayTo/Device.cs

@@ -34,7 +34,7 @@ namespace Emby.Dlna.PlayTo
         {
         {
             get
             get
             {
             {
-                RefreshVolumeIfNeeded();
+                RefreshVolumeIfNeeded().GetAwaiter().GetResult();
                 return _volume;
                 return _volume;
             }
             }
             set => _volume = value;
             set => _volume = value;
@@ -76,24 +76,24 @@ namespace Emby.Dlna.PlayTo
 
 
         private DateTime _lastVolumeRefresh;
         private DateTime _lastVolumeRefresh;
         private bool _volumeRefreshActive;
         private bool _volumeRefreshActive;
-        private void RefreshVolumeIfNeeded()
+        private Task RefreshVolumeIfNeeded()
         {
         {
-            if (!_volumeRefreshActive)
-            {
-                return;
-            }
-
-            if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
+            if (_volumeRefreshActive
+                && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
             {
             {
                 _lastVolumeRefresh = DateTime.UtcNow;
                 _lastVolumeRefresh = DateTime.UtcNow;
-                RefreshVolume(CancellationToken.None);
+                return RefreshVolume();
             }
             }
+
+            return Task.CompletedTask;
         }
         }
 
 
-        private async void RefreshVolume(CancellationToken cancellationToken)
+        private async Task RefreshVolume(CancellationToken cancellationToken = default)
         {
         {
             if (_disposed)
             if (_disposed)
+            {
                 return;
                 return;
+            }
 
 
             try
             try
             {
             {

+ 12 - 7
Emby.Dlna/PlayTo/PlayToController.cs

@@ -146,11 +146,14 @@ namespace Emby.Dlna.PlayTo
                 {
                 {
                     var positionTicks = GetProgressPositionTicks(streamInfo);
                     var positionTicks = GetProgressPositionTicks(streamInfo);
 
 
-                    ReportPlaybackStopped(streamInfo, positionTicks);
+                    await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
                 }
                 }
 
 
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
                 streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
 
                 var newItemProgress = GetProgressInfo(streamInfo);
                 var newItemProgress = GetProgressInfo(streamInfo);
 
 
@@ -173,11 +176,14 @@ namespace Emby.Dlna.PlayTo
             {
             {
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
                 var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
 
 
-                if (streamInfo.Item == null) return;
+                if (streamInfo.Item == null)
+                {
+                    return;
+                }
 
 
                 var positionTicks = GetProgressPositionTicks(streamInfo);
                 var positionTicks = GetProgressPositionTicks(streamInfo);
 
 
-                ReportPlaybackStopped(streamInfo, positionTicks);
+                await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
 
 
                 var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
                 var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
 
 
@@ -185,7 +191,7 @@ namespace Emby.Dlna.PlayTo
                     (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
                     (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
                     mediaSource.RunTimeTicks;
                     mediaSource.RunTimeTicks;
 
 
-                var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
+                var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
 
 
                 if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
                 if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
                 {
                 {
@@ -210,7 +216,7 @@ namespace Emby.Dlna.PlayTo
             }
             }
         }
         }
 
 
-        private async void ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
+        private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
         {
         {
             try
             try
             {
             {
@@ -220,7 +226,6 @@ namespace Emby.Dlna.PlayTo
                     SessionId = _session.Id,
                     SessionId = _session.Id,
                     PositionTicks = positionTicks,
                     PositionTicks = positionTicks,
                     MediaSourceId = streamInfo.MediaSourceId
                     MediaSourceId = streamInfo.MediaSourceId
-
                 }).ConfigureAwait(false);
                 }).ConfigureAwait(false);
             }
             }
             catch (Exception ex)
             catch (Exception ex)

+ 6 - 10
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -100,15 +100,13 @@ namespace Emby.Dlna.Ssdp
 
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
                     Headers = headers,
                     LocalIpAddress = e.LocalIpAddress
                     LocalIpAddress = e.LocalIpAddress
-                }
-            };
+                });
 
 
             DeviceDiscoveredInternal?.Invoke(this, args);
             DeviceDiscoveredInternal?.Invoke(this, args);
         }
         }
@@ -121,14 +119,12 @@ namespace Emby.Dlna.Ssdp
 
 
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
             var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
 
 
-            var args = new GenericEventArgs<UpnpDeviceInfo>
-            {
-                Argument = new UpnpDeviceInfo
+            var args = new GenericEventArgs<UpnpDeviceInfo>(
+                new UpnpDeviceInfo
                 {
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers
                     Headers = headers
-                }
-            };
+                });
 
 
             DeviceLeft?.Invoke(this, args);
             DeviceLeft?.Invoke(this, args);
         }
         }

+ 4 - 10
Emby.Dlna/Ssdp/Extensions.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
+using System.Linq;
 using System.Xml.Linq;
 using System.Xml.Linq;
 
 
 namespace Emby.Dlna.Ssdp
 namespace Emby.Dlna.Ssdp
@@ -10,24 +11,17 @@ namespace Emby.Dlna.Ssdp
         {
         {
             var node = container.Element(name);
             var node = container.Element(name);
 
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
         }
 
 
         public static string GetAttributeValue(this XElement container, XName name)
         public static string GetAttributeValue(this XElement container, XName name)
         {
         {
             var node = container.Attribute(name);
             var node = container.Attribute(name);
 
 
-            return node == null ? null : node.Value;
+            return node?.Value;
         }
         }
 
 
         public static string GetDescendantValue(this XElement container, XName name)
         public static string GetDescendantValue(this XElement container, XName name)
-        {
-            foreach (var node in container.Descendants(name))
-            {
-                return node.Value;
-            }
-
-            return null;
-        }
+            => container.Descendants(name).FirstOrDefault()?.Value;
     }
     }
 }
 }

+ 23 - 2
Emby.Drawing/ImageProcessor.cs

@@ -114,7 +114,7 @@ namespace Emby.Drawing
             => _transparentImageTypes.Contains(Path.GetExtension(path));
             => _transparentImageTypes.Contains(Path.GetExtension(path));
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
+        public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
         {
         {
             ItemImageInfo originalImage = options.Image;
             ItemImageInfo originalImage = options.Image;
             BaseItem item = options.Item;
             BaseItem item = options.Item;
@@ -230,7 +230,7 @@ namespace Emby.Drawing
             return ImageFormat.Jpg;
             return ImageFormat.Jpg;
         }
         }
 
 
-        private string GetMimeType(ImageFormat format, string path)
+        private string? GetMimeType(ImageFormat format, string path)
             => format switch
             => format switch
             {
             {
                 ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
                 ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@@ -313,6 +313,27 @@ namespace Emby.Drawing
         public ImageDimensions GetImageDimensions(string path)
         public ImageDimensions GetImageDimensions(string path)
             => _imageEncoder.GetImageSize(path);
             => _imageEncoder.GetImageSize(path);
 
 
+        /// <inheritdoc />
+        public string GetImageBlurHash(string path)
+        {
+            var size = GetImageDimensions(path);
+            if (size.Width <= 0 || size.Height <= 0)
+            {
+                return string.Empty;
+            }
+
+            // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
+            // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
+            // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
+            float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
+            float yCompF = xCompF * size.Height / size.Width;
+
+            int xComp = Math.Min((int)xCompF + 1, 9);
+            int yComp = Math.Min((int)yCompF + 1, 9);
+
+            return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
+        }
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);

+ 6 - 0
Emby.Drawing/NullImageEncoder.cs

@@ -42,5 +42,11 @@ namespace Emby.Drawing
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
+
+        /// <inheritdoc />
+        public string GetImageBlurHash(int xComp, int yComp, string path)
+        {
+            throw new NotImplementedException();
+        }
     }
     }
 }
 }

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

@@ -142,7 +142,7 @@ namespace Emby.Naming.Common
 
 
             CleanStrings = new[]
             CleanStrings = new[]
             {
             {
-                @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+                @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
                 @"(\[.*\])"
                 @"(\[.*\])"
             };
             };
 
 

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

@@ -45,6 +45,7 @@ using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.Updates;
+using Emby.Server.Implementations.SyncPlay;
 using MediaBrowser.Api;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -78,6 +79,7 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.TV;
+using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
@@ -613,6 +615,8 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
 
+            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+
             serviceCollection.AddSingleton<LiveTvDtoService>();
             serviceCollection.AddSingleton<LiveTvDtoService>();
             serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
             serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
 

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

@@ -46,14 +46,14 @@ namespace Emby.Server.Implementations.Channels
             new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
             new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
 
 
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
-        
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.
         /// </summary>
         /// </summary>
         /// <param name="userManager">The user manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="dtoService">The dto service.</param>
         /// <param name="dtoService">The dto service.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="loggerFactory">The logger factory.</param>
+        /// <param name="logger">The logger.</param>
         /// <param name="config">The server configuration manager.</param>
         /// <param name="config">The server configuration manager.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="userDataManager">The user data manager.</param>

+ 1 - 1
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Configuration
             ValidateMetadataPath(newConfig);
             ValidateMetadataPath(newConfig);
             ValidateSslCertificate(newConfig);
             ValidateSslCertificate(newConfig);
 
 
-            ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig });
+            ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
 
 
             base.ReplaceConfiguration(newConfiguration);
             base.ReplaceConfiguration(newConfiguration);
         }
         }

+ 2 - 2
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
@@ -129,8 +131,6 @@ namespace Emby.Server.Implementations.Cryptography
                 _randomNumberGenerator.Dispose();
                 _randomNumberGenerator.Dispose();
             }
             }
 
 
-            _randomNumberGenerator = null;
-
             _disposed = true;
             _disposed = true;
         }
         }
     }
     }

+ 21 - 13
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
@@ -33,7 +35,7 @@ using SQLitePCL.pretty;
 namespace Emby.Server.Implementations.Data
 namespace Emby.Server.Implementations.Data
 {
 {
     /// <summary>
     /// <summary>
-    /// Class SQLiteItemRepository
+    /// Class SQLiteItemRepository.
     /// </summary>
     /// </summary>
     public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
     public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
     {
     {
@@ -1141,24 +1143,24 @@ namespace Emby.Server.Implementations.Data
 
 
         public string ToValueString(ItemImageInfo image)
         public string ToValueString(ItemImageInfo image)
         {
         {
-            var delimeter = "*";
-
-            var path = image.Path;
+            const string Delimeter = "*";
 
 
-            if (path == null)
-            {
-                path = string.Empty;
-            }
+            var path = image.Path ?? string.Empty;
+            var hash = image.BlurHash ?? string.Empty;
 
 
             return GetPathToSave(path) +
             return GetPathToSave(path) +
-                   delimeter +
+                   Delimeter +
                    image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
                    image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
-                   delimeter +
+                   Delimeter +
                    image.Type +
                    image.Type +
-                   delimeter +
+                   Delimeter +
                    image.Width.ToString(CultureInfo.InvariantCulture) +
                    image.Width.ToString(CultureInfo.InvariantCulture) +
-                   delimeter +
-                   image.Height.ToString(CultureInfo.InvariantCulture);
+                   Delimeter +
+                   image.Height.ToString(CultureInfo.InvariantCulture) +
+                   Delimeter +
+                   // Replace delimiters with other characters.
+                   // This can be removed when we migrate to a proper DB.
+                   hash.Replace('*', '/').Replace('|', '\\');
         }
         }
 
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -1192,6 +1194,11 @@ namespace Emby.Server.Implementations.Data
                     image.Width = width;
                     image.Width = width;
                     image.Height = height;
                     image.Height = height;
                 }
                 }
+
+                if (parts.Length >= 6)
+                {
+                    image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|');
+                }
             }
             }
 
 
             return image;
             return image;
@@ -1971,6 +1978,7 @@ namespace Emby.Server.Implementations.Data
         /// Gets the chapter.
         /// Gets the chapter.
         /// </summary>
         /// </summary>
         /// <param name="reader">The reader.</param>
         /// <param name="reader">The reader.</param>
+        /// <param name="item">The item.</param>
         /// <returns>ChapterInfo.</returns>
         /// <returns>ChapterInfo.</returns>
         private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item)
         private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item)
         {
         {

+ 1 - 7
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -62,13 +62,7 @@ namespace Emby.Server.Implementations.Devices
         {
         {
             _authRepo.UpdateDeviceOptions(deviceId, options);
             _authRepo.UpdateDeviceOptions(deviceId, options);
 
 
-            if (DeviceOptionsUpdated != null)
-            {
-                DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>()
-                {
-                    Argument = new Tuple<string, DeviceOptions>(deviceId, options)
-                });
-            }
+            DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
         }
         }
 
 
         public DeviceOptions GetDeviceOptions(string deviceId)
         public DeviceOptions GetDeviceOptions(string deviceId)

+ 79 - 15
Emby.Server.Implementations/Dto/DtoService.cs

@@ -605,7 +605,7 @@ namespace Emby.Server.Implementations.Dto
 
 
                 if (dictionary.TryGetValue(person.Name, out Person entity))
                 if (dictionary.TryGetValue(person.Name, out Person entity))
                 {
                 {
-                    baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
+                    baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
                     baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
                     baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
                     list.Add(baseItemPerson);
                     list.Add(baseItemPerson);
                 }
                 }
@@ -654,6 +654,70 @@ namespace Emby.Server.Implementations.Dto
             return _libraryManager.GetGenreId(name);
             return _libraryManager.GetGenreId(name);
         }
         }
 
 
+        private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
+        {
+            var image = item.GetImageInfo(imageType, imageIndex);
+            if (image != null)
+            {
+                return GetTagAndFillBlurhash(dto, item, image);
+            }
+
+            return null;
+        }
+
+        private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
+        {
+            var tag = GetImageCacheTag(item, image);
+            if (!string.IsNullOrEmpty(image.BlurHash))
+            {
+                if (dto.ImageBlurHashes == null)
+                {
+                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                }
+
+                if (!dto.ImageBlurHashes.ContainsKey(image.Type))
+                {
+                    dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>();
+                }
+
+                dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
+            }
+
+            return tag;
+        }
+
+        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
+        {
+            return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
+        }
+
+        private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInfo> images)
+        {
+            var tags = GetImageTags(item, images);
+            var hashes = new Dictionary<string, string>();
+            for (int i = 0; i < images.Count; i++)
+            {
+                var img = images[i];
+                if (!string.IsNullOrEmpty(img.BlurHash))
+                {
+                    var tag = tags[i];
+                    hashes[tag] = img.BlurHash;
+                }
+            }
+
+            if (hashes.Count > 0)
+            {
+                if (dto.ImageBlurHashes == null)
+                {
+                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                }
+
+                dto.ImageBlurHashes[imageType] = hashes;
+            }
+
+            return tags;
+        }
+
         /// <summary>
         /// <summary>
         /// Sets simple property values on a DTOBaseItem
         /// Sets simple property values on a DTOBaseItem
         /// </summary>
         /// </summary>
@@ -674,8 +738,8 @@ namespace Emby.Server.Implementations.Dto
                 dto.LockData = item.IsLocked;
                 dto.LockData = item.IsLocked;
                 dto.ForcedSortName = item.ForcedSortName;
                 dto.ForcedSortName = item.ForcedSortName;
             }
             }
-            dto.Container = item.Container;
 
 
+            dto.Container = item.Container;
             dto.EndDate = item.EndDate;
             dto.EndDate = item.EndDate;
 
 
             if (options.ContainsField(ItemFields.ExternalUrls))
             if (options.ContainsField(ItemFields.ExternalUrls))
@@ -694,10 +758,12 @@ namespace Emby.Server.Implementations.Dto
                 dto.AspectRatio = hasAspectRatio.AspectRatio;
                 dto.AspectRatio = hasAspectRatio.AspectRatio;
             }
             }
 
 
+            dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             if (backdropLimit > 0)
             if (backdropLimit > 0)
             {
             {
-                dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList());
+                dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
             }
             }
 
 
             if (options.ContainsField(ItemFields.ScreenshotImageTags))
             if (options.ContainsField(ItemFields.ScreenshotImageTags))
@@ -705,7 +771,7 @@ namespace Emby.Server.Implementations.Dto
                 var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
                 var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
                 if (screenshotLimit > 0)
                 if (screenshotLimit > 0)
                 {
                 {
-                    dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList());
+                    dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
                 }
                 }
             }
             }
 
 
@@ -721,12 +787,11 @@ namespace Emby.Server.Implementations.Dto
 
 
                 // Prevent implicitly captured closure
                 // Prevent implicitly captured closure
                 var currentItem = item;
                 var currentItem = item;
-                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
-                    .ToList())
+                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
                 {
                 {
                     if (options.GetImageLimit(image.Type) > 0)
                     if (options.GetImageLimit(image.Type) > 0)
                     {
                     {
-                        var tag = GetImageCacheTag(item, image);
+                        var tag = GetTagAndFillBlurhash(dto, item, image);
 
 
                         if (tag != null)
                         if (tag != null)
                         {
                         {
@@ -871,8 +936,7 @@ namespace Emby.Server.Implementations.Dto
                 if (albumParent != null)
                 if (albumParent != null)
                 {
                 {
                     dto.AlbumId = albumParent.Id;
                     dto.AlbumId = albumParent.Id;
-
-                    dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary);
+                    dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
                 }
                 }
 
 
                 //if (options.ContainsField(ItemFields.MediaSourceCount))
                 //if (options.ContainsField(ItemFields.MediaSourceCount))
@@ -1099,7 +1163,7 @@ namespace Emby.Server.Implementations.Dto
                     episodeSeries = episodeSeries ?? episode.Series;
                     episodeSeries = episodeSeries ?? episode.Series;
                     if (episodeSeries != null)
                     if (episodeSeries != null)
                     {
                     {
-                        dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary);
+                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
                     }
                     }
                 }
                 }
 
 
@@ -1145,7 +1209,7 @@ namespace Emby.Server.Implementations.Dto
                     series = series ?? season.Series;
                     series = series ?? season.Series;
                     if (series != null)
                     if (series != null)
                     {
                     {
-                        dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary);
+                        dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
                     }
                     }
                 }
                 }
             }
             }
@@ -1275,7 +1339,7 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     if (image != null)
                     {
                     {
                         dto.ParentLogoItemId = GetDtoId(parent);
                         dto.ParentLogoItemId = GetDtoId(parent);
-                        dto.ParentLogoImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                     }
                 }
                 }
                 if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null)
                 if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null)
@@ -1285,7 +1349,7 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     if (image != null)
                     {
                     {
                         dto.ParentArtItemId = GetDtoId(parent);
                         dto.ParentArtItemId = GetDtoId(parent);
-                        dto.ParentArtImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                     }
                 }
                 }
                 if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
                 if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
@@ -1295,7 +1359,7 @@ namespace Emby.Server.Implementations.Dto
                     if (image != null)
                     if (image != null)
                     {
                     {
                         dto.ParentThumbItemId = GetDtoId(parent);
                         dto.ParentThumbItemId = GetDtoId(parent);
-                        dto.ParentThumbImageTag = GetImageCacheTag(parent, image);
+                        dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
                     }
                     }
                 }
                 }
                 if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0)))
                 if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0)))
@@ -1305,7 +1369,7 @@ namespace Emby.Server.Implementations.Dto
                     if (images.Count > 0)
                     if (images.Count > 0)
                     {
                     {
                         dto.ParentBackdropItemId = GetDtoId(parent);
                         dto.ParentBackdropItemId = GetDtoId(parent);
-                        dto.ParentBackdropImageTags = GetImageTags(parent, images);
+                        dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
                     }
                     }
                 }
                 }
 
 

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

@@ -54,6 +54,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

+ 2 - 2
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -302,7 +302,7 @@ namespace Emby.Server.Implementations.EntryPoints
                                     .Select(x => x.First())
                                     .Select(x => x.First())
                                     .ToList();
                                     .ToList();
 
 
-                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None);
+                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
 
 
                 if (LibraryUpdateTimer != null)
                 if (LibraryUpdateTimer != null)
                 {
                 {
@@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <param name="foldersAddedTo">The folders added to.</param>
         /// <param name="foldersAddedTo">The folders added to.</param>
         /// <param name="foldersRemovedFrom">The folders removed from.</param>
         /// <param name="foldersRemovedFrom">The folders removed from.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        private async void SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
+        private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
         {
         {
             var userIds = _sessionManager.Sessions
             var userIds = _sessionManager.Sessions
                 .Select(i => i.UserId)
                 .Select(i => i.UserId)

+ 9 - 9
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -42,27 +42,27 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
-            SendMessage("SeriesTimerCreated", e.Argument);
+            await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
         }
         }
 
 
-        private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
-            SendMessage("TimerCreated", e.Argument);
+            await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
         }
         }
 
 
-        private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
-            SendMessage("SeriesTimerCancelled", e.Argument);
+            await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
         }
         }
 
 
-        private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
         {
-            SendMessage("TimerCancelled", e.Argument);
+            await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
         }
         }
 
 
-        private async void SendMessage(string name, TimerEventInfo info)
+        private async Task SendMessage(string name, TimerEventInfo info)
         {
         {
             var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
             var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
 
 

+ 20 - 20
Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -86,29 +86,29 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private void OnPackageInstalling(object sender, InstallationInfo e)
+        private async void OnPackageInstalling(object sender, InstallationInfo e)
         {
         {
             SendMessageToAdminSessions("PackageInstalling", e);
             SendMessageToAdminSessions("PackageInstalling", e);
         }
         }
 
 
-        private void OnPackageInstallationCancelled(object sender, InstallationInfo e)
+        private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
         {
         {
             SendMessageToAdminSessions("PackageInstallationCancelled", e);
             SendMessageToAdminSessions("PackageInstallationCancelled", e);
         }
         }
 
 
-        private void OnPackageInstallationCompleted(object sender, InstallationInfo e)
+        private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
         {
         {
             SendMessageToAdminSessions("PackageInstallationCompleted", e);
             SendMessageToAdminSessions("PackageInstallationCompleted", e);
         }
         }
 
 
-        private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+        private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
         {
         {
-            SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo);
+            await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
         }
         }
 
 
-        private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+        private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
         {
         {
-            SendMessageToAdminSessions("ScheduledTaskEnded", e.Result);
+            await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
         /// <param name="e">The e.</param>
-        private void OnPluginUninstalled(object sender, IPlugin e)
+        private async void OnPluginUninstalled(object sender, IPlugin e)
         {
         {
             SendMessageToAdminSessions("PluginUninstalled", e);
             SendMessageToAdminSessions("PluginUninstalled", e);
         }
         }
@@ -126,9 +126,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
         /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        private void OnHasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnHasPendingRestartChanged(object sender, EventArgs e)
         {
         {
-            _sessionManager.SendRestartRequiredNotification(CancellationToken.None);
+            await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -136,11 +136,11 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
         /// <param name="e">The e.</param>
-        private void OnUserUpdated(object sender, GenericEventArgs<User> e)
+        private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
         {
         {
             var dto = _userManager.GetUserDto(e.Argument);
             var dto = _userManager.GetUserDto(e.Argument);
 
 
-            SendMessageToUserSession(e.Argument, "UserUpdated", dto);
+            await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -148,26 +148,26 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
         /// <param name="e">The e.</param>
-        private void OnUserDeleted(object sender, GenericEventArgs<User> e)
+        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
         {
         {
-            SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture));
+            await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
         }
         }
 
 
-        private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
+        private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
         {
         {
             var dto = _userManager.GetUserDto(e.Argument);
             var dto = _userManager.GetUserDto(e.Argument);
 
 
-            SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto);
+            await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false);
         }
         }
 
 
-        private void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
+        private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
         {
         {
             var dto = _userManager.GetUserDto(e.Argument);
             var dto = _userManager.GetUserDto(e.Argument);
 
 
-            SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto);
+            await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false);
         }
         }
 
 
-        private async void SendMessageToAdminSessions<T>(string name, T data)
+        private async Task SendMessageToAdminSessions<T>(string name, T data)
         {
         {
             try
             try
             {
             {
@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             }
         }
         }
 
 
-        private async void SendMessageToUserSession<T>(User user, string name, T data)
+        private async Task SendMessageToUserSession<T>(User user, string name, T data)
         {
         {
             try
             try
             {
             {

+ 8 - 4
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using Emby.Server.Implementations.Udp;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
@@ -22,6 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _config;
 
 
         /// <summary>
         /// <summary>
         /// The UDP server.
         /// The UDP server.
@@ -35,19 +37,21 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         public UdpServerEntryPoint(
         public UdpServerEntryPoint(
             ILogger<UdpServerEntryPoint> logger,
             ILogger<UdpServerEntryPoint> logger,
-            IServerApplicationHost appHost)
+            IServerApplicationHost appHost,
+            IConfiguration configuration)
         {
         {
             _logger = logger;
             _logger = logger;
             _appHost = appHost;
             _appHost = appHost;
-
+            _config = configuration;
 
 
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task RunAsync()
+        public Task RunAsync()
         {
         {
-            _udpServer = new UdpServer(_logger, _appHost);
+            _udpServer = new UdpServer(_logger, _appHost, _config);
             _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
             _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            return Task.CompletedTask;
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />

+ 9 - 4
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -56,6 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// <summary>
         /// Gets the result.
         /// Gets the result.
         /// </summary>
         /// </summary>
+        /// <param name="requestContext">The request context.</param>
         /// <param name="content">The content.</param>
         /// <param name="content">The content.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="responseHeaders">The response headers.</param>
@@ -255,16 +256,20 @@ namespace Emby.Server.Implementations.HttpServer
         {
         {
             var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
             var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
 
 
-            if (string.IsNullOrEmpty(acceptEncoding))
+            if (!string.IsNullOrEmpty(acceptEncoding))
             {
             {
-                //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
+                // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
                 //    return "br";
                 //    return "br";
 
 
-                if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
+                if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
+                {
                     return "deflate";
                     return "deflate";
+                }
 
 
-                if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+                if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
+                {
                     return "gzip";
                     return "gzip";
+                }
             }
             }
 
 
             return null;
             return null;

+ 39 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -78,6 +78,9 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The last activity date.</value>
         /// <value>The last activity date.</value>
         public DateTime LastActivityDate { get; private set; }
         public DateTime LastActivityDate { get; private set; }
 
 
+        /// <inheritdoc />
+        public DateTime LastKeepAliveDate { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the query string.
         /// Gets or sets the query string.
         /// </summary>
         /// </summary>
@@ -218,7 +221,42 @@ namespace Emby.Server.Implementations.HttpServer
                 Connection = this
                 Connection = this
             };
             };
 
 
-            await OnReceive(info).ConfigureAwait(false);
+            if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+            {
+                await SendKeepAliveResponse();
+            }
+            else
+            {
+                await OnReceive(info).ConfigureAwait(false);
+            }
+        }
+
+        private Task SendKeepAliveResponse()
+        {
+            LastKeepAliveDate = DateTime.UtcNow;
+            return SendAsync(new WebSocketMessage<string>
+            {
+                MessageType = "KeepAlive"
+            }, CancellationToken.None);
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                _socket.Dispose();
+            }
         }
         }
     }
     }
 }
 }

+ 9 - 0
Emby.Server.Implementations/IStartupOptions.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+
+using System;
+
 namespace Emby.Server.Implementations
 namespace Emby.Server.Implementations
 {
 {
     public interface IStartupOptions
     public interface IStartupOptions
@@ -36,5 +40,10 @@ namespace Emby.Server.Implementations
         /// Gets the value of the --plugin-manifest-url command line option.
         /// Gets the value of the --plugin-manifest-url command line option.
         /// </summary>
         /// </summary>
         string PluginManifestUrl { get; }
         string PluginManifestUrl { get; }
+
+        /// <summary>
+        /// Gets the value of the --published-server-url command line option.
+        /// </summary>
+        Uri PublishedServerUrl { get; }
     }
     }
 }
 }

+ 0 - 38
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -135,43 +135,5 @@ namespace Emby.Server.Implementations.Library
                 ? null
                 ? null
                 : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
                 : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
         }
         }
-
-        /// <summary>
-        /// Gets the hashed string.
-        /// </summary>
-        public string GetHashedString(User user, string str)
-        {
-            if (string.IsNullOrEmpty(user.Password))
-            {
-                return _cryptographyProvider.CreatePasswordHash(str).ToString();
-            }
-
-            // TODO: make use of iterations parameter?
-            PasswordHash passwordHash = PasswordHash.Parse(user.Password);
-            var salt = passwordHash.Salt.ToArray();
-            return new PasswordHash(
-                passwordHash.Id,
-                _cryptographyProvider.ComputeHash(
-                    passwordHash.Id,
-                    Encoding.UTF8.GetBytes(str),
-                    salt),
-                salt,
-                passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString();
-        }
-
-        public ReadOnlySpan<byte> GetHashed(User user, string str)
-        {
-            if (string.IsNullOrEmpty(user.Password))
-            {
-                return _cryptographyProvider.CreatePasswordHash(str).Hash;
-            }
-
-            // TODO: make use of iterations parameter?
-            PasswordHash passwordHash = PasswordHash.Parse(user.Password);
-            return _cryptographyProvider.ComputeHash(
-                    passwordHash.Id,
-                    Encoding.UTF8.GetBytes(str),
-                    passwordHash.Salt.ToArray());
-        }
     }
     }
 }
 }

+ 97 - 5
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -21,6 +21,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -35,6 +36,7 @@ using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -50,7 +52,7 @@ using VideoResolver = Emby.Naming.Video.VideoResolver;
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
 {
 {
     /// <summary>
     /// <summary>
-    /// Class LibraryManager
+    /// Class LibraryManager.
     /// </summary>
     /// </summary>
     public class LibraryManager : ILibraryManager
     public class LibraryManager : ILibraryManager
     {
     {
@@ -67,6 +69,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IItemRepository _itemRepository;
         private readonly IItemRepository _itemRepository;
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+        private readonly IImageProcessor _imageProcessor;
 
 
         private NamingOptions _namingOptions;
         private NamingOptions _namingOptions;
         private string[] _videoFileExtensions;
         private string[] _videoFileExtensions;
@@ -135,6 +138,13 @@ namespace Emby.Server.Implementations.Library
         /// <param name="userManager">The user manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
         /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryMonitorFactory">The library monitor.</param>
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="providerManagerFactory">The provider manager.</param>
+        /// <param name="userviewManagerFactory">The userview manager.</param>
+        /// <param name="mediaEncoder">The media encoder.</param>
+        /// <param name="itemRepository">The item repository.</param>
+        /// <param name="imageProcessor">The image processor.</param>
         public LibraryManager(
         public LibraryManager(
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ILogger<LibraryManager> logger,
             ILogger<LibraryManager> logger,
@@ -147,7 +157,8 @@ namespace Emby.Server.Implementations.Library
             Lazy<IProviderManager> providerManagerFactory,
             Lazy<IProviderManager> providerManagerFactory,
             Lazy<IUserViewManager> userviewManagerFactory,
             Lazy<IUserViewManager> userviewManagerFactory,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
-            IItemRepository itemRepository)
+            IItemRepository itemRepository,
+            IImageProcessor imageProcessor)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
             _logger = logger;
             _logger = logger;
@@ -161,6 +172,7 @@ namespace Emby.Server.Implementations.Library
             _userviewManagerFactory = userviewManagerFactory;
             _userviewManagerFactory = userviewManagerFactory;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _itemRepository = itemRepository;
             _itemRepository = itemRepository;
+            _imageProcessor = imageProcessor;
 
 
             _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
             _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
 
 
@@ -1815,10 +1827,90 @@ namespace Emby.Server.Implementations.Library
             }
             }
         }
         }
 
 
-        public void UpdateImages(BaseItem item)
+        private bool ImageNeedsRefresh(ItemImageInfo image)
         {
         {
-            _itemRepository.SaveImages(item);
+            if (image.Path != null && image.IsLocalFile)
+            {
+                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
+                {
+                    return true;
+                }
 
 
+                try
+                {
+                    return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
+                    return false;
+                }
+            }
+
+            return image.Path != null && !image.IsLocalFile;
+        }
+
+        public void UpdateImages(BaseItem item, bool forceUpdate = false)
+        {
+            if (item == null)
+            {
+                throw new ArgumentNullException(nameof(item));
+            }
+
+            var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
+            if (outdated.Length == 0)
+            {
+                RegisterItem(item);
+                return;
+            }
+
+            foreach (var img in outdated)
+            {
+                var image = img;
+                if (!img.IsLocalFile)
+                {
+                    try
+                    {
+                        var index = item.GetImageIndex(img);
+                        image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult();
+                    }
+                    catch (ArgumentException)
+                    {
+                        _logger.LogWarning("Cannot get image index for {0}", img.Path);
+                        continue;
+                    }
+                    catch (InvalidOperationException)
+                    {
+                        _logger.LogWarning("Cannot fetch image from {0}", img.Path);
+                        continue;
+                    }
+                }
+
+                ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
+                image.Width = size.Width;
+                image.Height = size.Height;
+
+                try
+                {
+                    image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
+                    image.BlurHash = string.Empty;
+                }
+
+                try
+                {
+                    image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
+                }
+            }
+
+            _itemRepository.SaveImages(item);
             RegisterItem(item);
             RegisterItem(item);
         }
         }
 
 
@@ -1839,7 +1931,7 @@ namespace Emby.Server.Implementations.Library
 
 
                 item.DateLastSaved = DateTime.UtcNow;
                 item.DateLastSaved = DateTime.UtcNow;
 
 
-                RegisterItem(item);
+                UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
             }
 
 
             _itemRepository.SaveItems(itemsList, cancellationToken);
             _itemRepository.SaveItems(itemsList, cancellationToken);

+ 1 - 5
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -521,11 +521,7 @@ namespace Emby.Server.Implementations.Library
                 SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
                 SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
             }
             }
 
 
-            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse
-            {
-                MediaSource = clone
-
-            }, liveStream as IDirectStreamProvider);
+            return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
         }
         }
 
 
         private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
         private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)

+ 5 - 30
Emby.Server.Implementations/Library/UserManager.cs

@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         private void OnUserUpdated(User user)
         private void OnUserUpdated(User user)
         {
         {
-            UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+            UserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         private void OnUserDeleted(User user)
         private void OnUserDeleted(User user)
         {
         {
-            UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+            UserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
         }
         }
 
 
         public NameIdPair[] GetAuthenticationProviders()
         public NameIdPair[] GetAuthenticationProviders()
@@ -608,31 +608,6 @@ namespace Emby.Server.Implementations.Library
             return dto;
             return dto;
         }
         }
 
 
-        public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            IAuthenticationProvider authenticationProvider = GetAuthenticationProvider(user);
-            bool hasConfiguredPassword = authenticationProvider.HasPassword(user);
-            bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(authenticationProvider.GetEasyPasswordHash(user));
-
-            bool hasPassword = user.Configuration.EnableLocalPassword &&
-                !string.IsNullOrEmpty(remoteEndPoint) &&
-                _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword;
-
-            PublicUserDto dto = new PublicUserDto
-            {
-                Name = user.Name,
-                HasPassword = hasPassword,
-                HasConfiguredPassword = hasConfiguredPassword,
-            };
-
-            return dto;
-        }
-
         public UserDto GetOfflineUserDto(User user)
         public UserDto GetOfflineUserDto(User user)
         {
         {
             var dto = GetUserDto(user);
             var dto = GetUserDto(user);
@@ -776,7 +751,7 @@ namespace Emby.Server.Implementations.Library
 
 
             _userRepository.CreateUser(user);
             _userRepository.CreateUser(user);
 
 
-            EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
+            EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User>(user), _logger);
 
 
             return user;
             return user;
         }
         }
@@ -1001,7 +976,7 @@ namespace Emby.Server.Implementations.Library
 
 
             if (fireEvent)
             if (fireEvent)
             {
             {
-                UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+                UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User>(user));
             }
             }
         }
         }
 
 
@@ -1071,7 +1046,7 @@ namespace Emby.Server.Implementations.Library
 
 
             if (fireEvent)
             if (fireEvent)
             {
             {
-                UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+                UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User>(user));
             }
             }
         }
         }
     }
     }

+ 4 - 9
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -140,11 +140,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
         {
         {
             if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
             {
             {
-                OnRecordingFoldersChanged();
+                await CreateRecordingFolders().ConfigureAwait(false);
             }
             }
         }
         }
 
 
@@ -155,11 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return CreateRecordingFolders();
             return CreateRecordingFolders();
         }
         }
 
 
-        private async void OnRecordingFoldersChanged()
-        {
-            await CreateRecordingFolders().ConfigureAwait(false);
-        }
-
         internal async Task CreateRecordingFolders()
         internal async Task CreateRecordingFolders()
         {
         {
             try
             try
@@ -1334,7 +1329,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     await CreateRecordingFolders().ConfigureAwait(false);
                     await CreateRecordingFolders().ConfigureAwait(false);
 
 
                     TriggerRefresh(recordPath);
                     TriggerRefresh(recordPath);
-                    EnforceKeepUpTo(timer, seriesPath);
+                    await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false);
                 };
                 };
 
 
                 await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
                 await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
@@ -1494,7 +1489,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return item;
             return item;
         }
         }
 
 
-        private async void EnforceKeepUpTo(TimerInfo timer, string seriesPath)
+        private async Task EnforceKeepUpTo(TimerInfo timer, string seriesPath)
         {
         {
             if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
             if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
             {
             {

+ 2 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             onStarted();
             onStarted();
 
 
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
-            StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
+            _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
 
 
             _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
             _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
 
 
@@ -321,7 +321,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private async void StartStreamingLog(Stream source, Stream target)
+        private async Task StartStreamingLog(Stream source, Stream target)
         {
         {
             try
             try
             {
             {

+ 2 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             if (startDate < now)
             if (startDate < now)
             {
             {
-                TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = item });
+                TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item));
                 return;
                 return;
             }
             }
 
 
@@ -151,7 +151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
             var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
             if (timer != null)
             if (timer != null)
             {
             {
-                TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = timer });
+                TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
             }
             }
         }
         }
 
 

+ 20 - 59
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -10,10 +10,8 @@ using Emby.Server.Implementations.Library;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
@@ -148,27 +146,18 @@ namespace Emby.Server.Implementations.LiveTv
         {
         {
             var timerId = e.Argument;
             var timerId = e.Argument;
 
 
-            TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-            {
-                Argument = new TimerEventInfo
-                {
-                    Id = timerId
-                }
-            });
+            TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId)));
         }
         }
 
 
         private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e)
         private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e)
         {
         {
             var timer = e.Argument;
             var timer = e.Argument;
 
 
-            TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-            {
-                Argument = new TimerEventInfo
+            TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+                new TimerEventInfo(timer.Id)
                 {
                 {
-                    ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId),
-                    Id = timer.Id
-                }
-            });
+                    ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId)
+                }));
         }
         }
 
 
         public List<NameIdPair> GetTunerHostTypes()
         public List<NameIdPair> GetTunerHostTypes()
@@ -788,22 +777,12 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (query.OrderBy.Count == 0)
             if (query.OrderBy.Count == 0)
             {
             {
-                if (query.IsAiring ?? false)
-                {
-                    // Unless something else was specified, order by start date to take advantage of a specialized index
-                    query.OrderBy = new[]
-                    {
-                        (ItemSortBy.StartDate, SortOrder.Ascending)
-                    };
-                }
-                else
+
+                // Unless something else was specified, order by start date to take advantage of a specialized index
+                query.OrderBy = new[]
                 {
                 {
-                    // Unless something else was specified, order by start date to take advantage of a specialized index
-                    query.OrderBy = new[]
-                    {
-                        (ItemSortBy.StartDate, SortOrder.Ascending)
-                    };
-                }
+                    (ItemSortBy.StartDate, SortOrder.Ascending)
+                };
             }
             }
 
 
             RemoveFields(options);
             RemoveFields(options);
@@ -1734,13 +1713,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (!(service is EmbyTV.EmbyTV))
             if (!(service is EmbyTV.EmbyTV))
             {
             {
-                TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-                {
-                    Argument = new TimerEventInfo
-                    {
-                        Id = id
-                    }
-                });
+                TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
             }
             }
         }
         }
 
 
@@ -1757,13 +1730,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
             await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
 
 
-            SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-            {
-                Argument = new TimerEventInfo
-                {
-                    Id = id
-                }
-            });
+            SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
         }
         }
 
 
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
@@ -2082,14 +2049,11 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             if (!(service is EmbyTV.EmbyTV))
             if (!(service is EmbyTV.EmbyTV))
             {
             {
-                TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-                {
-                    Argument = new TimerEventInfo
+                TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+                    new TimerEventInfo(newTimerId)
                     {
                     {
-                        ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
-                        Id = newTimerId
-                    }
-                });
+                        ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+                    }));
             }
             }
         }
         }
 
 
@@ -2114,14 +2078,11 @@ namespace Emby.Server.Implementations.LiveTv
                 await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
                 await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
-            SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
-            {
-                Argument = new TimerEventInfo
+            SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+                new TimerEventInfo(newTimerId)
                 {
                 {
-                    ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
-                    Id = newTimerId
-                }
-            });
+                    ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+                }));
         }
         }
 
 
         public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken)
         public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken)

+ 2 - 0
Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs

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

+ 117 - 0
Emby.Server.Implementations/Localization/Core/es_419.json

@@ -0,0 +1,117 @@
+{
+    "LabelRunningTimeValue": "Duración: {0}",
+    "ValueSpecialEpisodeName": "Especial - {0}",
+    "Sync": "Sincronizar",
+    "Songs": "Canciones",
+    "Shows": "Programas",
+    "Playlists": "Listas de reproducción",
+    "Photos": "Fotos",
+    "Movies": "Películas",
+    "HeaderNextUp": "A continuación",
+    "HeaderLiveTV": "TV en vivo",
+    "HeaderFavoriteSongs": "Canciones favoritas",
+    "HeaderFavoriteArtists": "Artistas favoritos",
+    "HeaderFavoriteAlbums": "Álbumes favoritos",
+    "HeaderFavoriteEpisodes": "Episodios favoritos",
+    "HeaderFavoriteShows": "Programas favoritos",
+    "HeaderContinueWatching": "Continuar viendo",
+    "HeaderAlbumArtists": "Artistas del álbum",
+    "Genres": "Géneros",
+    "Folders": "Carpetas",
+    "Favorites": "Favoritos",
+    "Collections": "Colecciones",
+    "Channels": "Canales",
+    "Books": "Libros",
+    "Artists": "Artistas",
+    "Albums": "Álbumes",
+    "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
+    "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
+    "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+    "TaskRefreshChannels": "Actualizar canales",
+    "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+    "TaskCleanTranscode": "Limpiar directorio de transcodificado",
+    "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
+    "TaskUpdatePlugins": "Actualizar complementos",
+    "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
+    "TaskRefreshPeople": "Actualizar personas",
+    "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
+    "TaskCleanLogs": "Limpiar directorio de registros",
+    "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.",
+    "TaskRefreshLibrary": "Escanear biblioteca de medios",
+    "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
+    "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
+    "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
+    "TaskCleanCache": "Limpiar directorio caché",
+    "TasksChannelsCategory": "Canales de Internet",
+    "TasksApplicationCategory": "Aplicación",
+    "TasksLibraryCategory": "Biblioteca",
+    "TasksMaintenanceCategory": "Mantenimiento",
+    "VersionNumber": "Versión {0}",
+    "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
+    "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
+    "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
+    "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
+    "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
+    "UserOnlineFromDevice": "{0} está en línea desde {1}",
+    "UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
+    "UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
+    "UserDownloadingItemWithValues": "{0} está descargando {1}",
+    "UserDeletedWithName": "El usuario {0} ha sido eliminado",
+    "UserCreatedWithName": "El usuario {0} ha sido creado",
+    "User": "Usuario",
+    "TvShows": "Programas de TV",
+    "System": "Sistema",
+    "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
+    "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
+    "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+    "ScheduledTaskStartedWithName": "{0} iniciado",
+    "ScheduledTaskFailedWithName": "{0} falló",
+    "ProviderValue": "Proveedor: {0}",
+    "PluginUpdatedWithName": "{0} fue actualizado",
+    "PluginUninstalledWithName": "{0} fue desinstalado",
+    "PluginInstalledWithName": "{0} fue instalado",
+    "Plugin": "Complemento",
+    "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
+    "NotificationOptionVideoPlayback": "Reproducción de video iniciada",
+    "NotificationOptionUserLockedOut": "Usuario bloqueado",
+    "NotificationOptionTaskFailed": "Falla de tarea programada",
+    "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
+    "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
+    "NotificationOptionPluginUninstalled": "Complemento desinstalado",
+    "NotificationOptionPluginInstalled": "Complemento instalado",
+    "NotificationOptionPluginError": "Falla de complemento",
+    "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
+    "NotificationOptionInstallationFailed": "Falla de instalación",
+    "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
+    "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
+    "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
+    "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
+    "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
+    "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
+    "NameSeasonUnknown": "Temporada desconocida",
+    "NameSeasonNumber": "Temporada {0}",
+    "NameInstallFailed": "Falló la instalación de {0}",
+    "MusicVideos": "Videos musicales",
+    "Music": "Música",
+    "MixedContent": "Contenido mezclado",
+    "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
+    "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
+    "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
+    "Latest": "Recientes",
+    "LabelIpAddressValue": "Dirección IP: {0}",
+    "ItemRemovedWithName": "{0} fue removido de la biblioteca",
+    "ItemAddedWithName": "{0} fue agregado a la biblioteca",
+    "Inherit": "Heredar",
+    "HomeVideos": "Videos caseros",
+    "HeaderRecordingGroups": "Grupos de grabación",
+    "HeaderCameraUploads": "Subidas desde la cámara",
+    "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
+    "DeviceOnlineWithName": "{0} está conectado",
+    "DeviceOfflineWithName": "{0} se ha desconectado",
+    "ChapterNameValue": "Capítulo {0}",
+    "CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
+    "AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
+    "Application": "Aplicación",
+    "AppDeviceValues": "App: {0}, Dispositivo: {1}"
+}

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

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "Books": "Livres",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}",
+    "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}",
     "Channels": "Chaînes",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
     "Collections": "Collections",
@@ -15,7 +15,7 @@
     "Favorites": "Favoris",
     "Favorites": "Favoris",
     "Folders": "Dossiers",
     "Folders": "Dossiers",
     "Genres": "Genres",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artistes de l'album",
+    "HeaderAlbumArtists": "Artistes",
     "HeaderCameraUploads": "Photos transférées",
     "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteAlbums": "Albums favoris",

+ 99 - 0
Emby.Server.Implementations/Localization/Core/ta.json

@@ -0,0 +1,99 @@
+{
+    "VersionNumber": "பதிப்பு {0}",
+    "ValueSpecialEpisodeName": "சிறப்பு - {0}",
+    "TasksMaintenanceCategory": "பராமரிப்பு",
+    "TaskCleanCache": "தற்காலிக சேமிப்பு கோப்பகத்தை சுத்தம் செய்யவும்",
+    "TaskRefreshChapterImages": "அத்தியாயப் படங்களை பிரித்தெடுக்கவும்",
+    "TaskRefreshPeople": "மக்களைப் புதுப்பிக்கவும்",
+    "TaskCleanTranscode": "டிரான்ஸ்கோட் கோப்பகத்தை சுத்தம் செய்யவும்",
+    "TaskRefreshChannelsDescription": "இணையச் சேனல் தகவல்களைப் புதுப்பிக்கிறது.",
+    "System": "ஒருங்கியம்",
+    "NotificationOptionTaskFailed": "திட்டமிடப்பட்ட பணி தோல்வியடைந்தது",
+    "NotificationOptionPluginUpdateInstalled": "உட்செருகி புதுப்பிக்கப்பட்டது",
+    "NotificationOptionPluginUninstalled": "உட்செருகி நீக்கப்பட்டது",
+    "NotificationOptionPluginInstalled": "உட்செருகி நிறுவப்பட்டது",
+    "NotificationOptionPluginError": "உட்செருகி செயலிழந்தது",
+    "NotificationOptionCameraImageUploaded": "புகைப்படம் பதிவேற்றப்பட்டது",
+    "MixedContent": "கலப்பு உள்ளடக்கங்கள்",
+    "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
+    "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
+    "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
+    "Inherit": "மரபரிமையாகப் பெறு",
+    "HeaderRecordingGroups": "பதிவு குழுக்கள்",
+    "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
+    "Folders": "கோப்புறைகள்",
+    "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
+    "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
+    "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
+    "Collections": "தொகுப்புகள்",
+    "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+    "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
+    "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
+    "TaskRefreshChannels": "சேனல்களை புதுப்பி",
+    "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
+    "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
+    "TasksChannelsCategory": "இணைய சேனல்கள்",
+    "TasksApplicationCategory": "செயலி",
+    "TasksLibraryCategory": "நூலகம்",
+    "UserPolicyUpdatedWithName": "பயனர் கொள்கை {0} இற்கு புதுப்பிக்கப்பட்டுள்ளது",
+    "UserPasswordChangedWithName": "{0} பயனருக்கு கடவுச்சொல் மாற்றப்பட்டுள்ளது",
+    "UserLockedOutWithName": "பயனர் {0} முடக்கப்பட்டார்",
+    "UserDownloadingItemWithValues": "{0} ஆல் {1} பதிவிறக்கப்படுகிறது",
+    "UserDeletedWithName": "பயனர் {0} நீக்கப்பட்டார்",
+    "UserCreatedWithName": "பயனர் {0} உருவாக்கப்பட்டார்",
+    "User": "பயனர்",
+    "TvShows": "தொலைக்காட்சித் தொடர்கள்",
+    "Sync": "ஒத்திசைவு",
+    "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
+    "Songs": "பாட்டுகள்",
+    "Shows": "தொடர்கள்",
+    "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
+    "ScheduledTaskStartedWithName": "{0} துவங்கியது",
+    "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
+    "ProviderValue": "வழங்குநர்: {0}",
+    "PluginUpdatedWithName": "{0} புதுப்பிக்கப்பட்டது",
+    "PluginUninstalledWithName": "{0} நீக்கப்பட்டது",
+    "PluginInstalledWithName": "{0} நிறுவப்பட்டது",
+    "Plugin": "உட்செருகி",
+    "Playlists": "தொடர் பட்டியல்கள்",
+    "Photos": "புகைப்படங்கள்",
+    "NotificationOptionVideoPlaybackStopped": "நிகழ்பட ஒளிபரப்பு நிறுத்தப்பட்டது",
+    "NotificationOptionVideoPlayback": "நிகழ்பட ஒளிபரப்பு துவங்கியது",
+    "NotificationOptionUserLockedOut": "பயனர் கணக்கு முடக்கப்பட்டது",
+    "NotificationOptionServerRestartRequired": "சேவையக மறுதொடக்கம் தேவை",
+    "NotificationOptionNewLibraryContent": "புதிய உள்ளடக்கங்கள் சேர்க்கப்பட்டன",
+    "NotificationOptionInstallationFailed": "நிறுவல் தோல்வியடைந்தது",
+    "NotificationOptionAudioPlaybackStopped": "ஒலி இசைத்தல் நிறுத்தப்பட்டது",
+    "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
+    "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
+    "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
+    "NameSeasonUnknown": "பருவம் அறியப்படாதவை",
+    "NameSeasonNumber": "பருவம் {0}",
+    "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
+    "MusicVideos": "இசைப்படங்கள்",
+    "Music": "இசை",
+    "Movies": "திரைப்படங்கள்",
+    "Latest": "புதியன",
+    "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
+    "LabelIpAddressValue": "ஐபி முகவரி: {0}",
+    "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
+    "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
+    "HeaderNextUp": "அடுத்ததாக",
+    "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
+    "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்",
+    "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
+    "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
+    "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
+    "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
+    "HeaderContinueWatching": "தொடர்ந்து பார்",
+    "HeaderAlbumArtists": "இசைக் கலைஞர்கள்",
+    "Genres": "வகைகள்",
+    "Favorites": "பிடித்தவை",
+    "ChapterNameValue": "அத்தியாயம் {0}",
+    "Channels": "சேனல்கள்",
+    "Books": "புத்தகங்கள்",
+    "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
+    "Artists": "கலைஞர்கள்",
+    "Application": "செயலி",
+    "Albums": "ஆல்பங்கள்"
+}

+ 2 - 0
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

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

+ 2 - 0
Emby.Server.Implementations/Net/SocketFactory.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Net;
 using System.Net;
 using System.Net.Sockets;
 using System.Net.Sockets;

+ 2 - 0
Emby.Server.Implementations/Net/UdpSocket.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Net;
 using System.Net;
 using System.Net.Sockets;
 using System.Net.Sockets;

+ 2 - 0
Emby.Server.Implementations/Networking/NetworkManager.cs

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

+ 2 - 0
Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;

+ 3 - 3
Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Emby.Server.Implementations.Images;
 using Emby.Server.Implementations.Images;
@@ -32,9 +34,7 @@ namespace Emby.Server.Implementations.Playlists
                 {
                 {
                     var subItem = i.Item2;
                     var subItem = i.Item2;
 
 
-                    var episode = subItem as Episode;
-
-                    if (episode != null)
+                    if (subItem is Episode episode)
                     {
                     {
                         var series = episode.Series;
                         var series = episode.Series;
                         if (series != null && series.HasImage(ImageType.Primary))
                         if (series != null && series.HasImage(ImageType.Primary))

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

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
@@ -153,10 +155,7 @@ namespace Emby.Server.Implementations.Playlists
                     });
                     });
                 }
                 }
 
 
-                return new PlaylistCreationResult
-                {
-                    Id = playlist.Id.ToString("N", CultureInfo.InvariantCulture)
-                };
+                return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture));
             }
             }
             finally
             finally
             {
             {

+ 2 - 0
Emby.Server.Implementations/ResourceFileManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.IO;
 using System.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;

+ 12 - 10
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -51,7 +53,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// </summary>
         /// </summary>
         /// <value>The task manager.</value>
         /// <value>The task manager.</value>
         private ITaskManager TaskManager { get; set; }
         private ITaskManager TaskManager { get; set; }
-        private readonly IFileSystem _fileSystem;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@@ -72,24 +73,28 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// or
         /// or
         /// logger
         /// logger
         /// </exception>
         /// </exception>
-        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem)
+        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
         {
         {
             if (scheduledTask == null)
             if (scheduledTask == null)
             {
             {
                 throw new ArgumentNullException(nameof(scheduledTask));
                 throw new ArgumentNullException(nameof(scheduledTask));
             }
             }
+
             if (applicationPaths == null)
             if (applicationPaths == null)
             {
             {
                 throw new ArgumentNullException(nameof(applicationPaths));
                 throw new ArgumentNullException(nameof(applicationPaths));
             }
             }
+
             if (taskManager == null)
             if (taskManager == null)
             {
             {
                 throw new ArgumentNullException(nameof(taskManager));
                 throw new ArgumentNullException(nameof(taskManager));
             }
             }
+
             if (jsonSerializer == null)
             if (jsonSerializer == null)
             {
             {
                 throw new ArgumentNullException(nameof(jsonSerializer));
                 throw new ArgumentNullException(nameof(jsonSerializer));
             }
             }
+
             if (logger == null)
             if (logger == null)
             {
             {
                 throw new ArgumentNullException(nameof(logger));
                 throw new ArgumentNullException(nameof(logger));
@@ -100,7 +105,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             TaskManager = taskManager;
             TaskManager = taskManager;
             JsonSerializer = jsonSerializer;
             JsonSerializer = jsonSerializer;
             Logger = logger;
             Logger = logger;
-            _fileSystem = fileSystem;
 
 
             InitTriggerEvents();
             InitTriggerEvents();
         }
         }
@@ -392,7 +396,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
 
             ((TaskManager)TaskManager).OnTaskExecuting(this);
             ((TaskManager)TaskManager).OnTaskExecuting(this);
 
 
-            progress.ProgressChanged += progress_ProgressChanged;
+            progress.ProgressChanged += OnProgressChanged;
 
 
             TaskCompletionStatus status;
             TaskCompletionStatus status;
             CurrentExecutionStartTime = DateTime.UtcNow;
             CurrentExecutionStartTime = DateTime.UtcNow;
@@ -426,7 +430,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
             var startTime = CurrentExecutionStartTime;
             var startTime = CurrentExecutionStartTime;
             var endTime = DateTime.UtcNow;
             var endTime = DateTime.UtcNow;
 
 
-            progress.ProgressChanged -= progress_ProgressChanged;
+            progress.ProgressChanged -= OnProgressChanged;
             CurrentCancellationTokenSource.Dispose();
             CurrentCancellationTokenSource.Dispose();
             CurrentCancellationTokenSource = null;
             CurrentCancellationTokenSource = null;
             CurrentProgress = null;
             CurrentProgress = null;
@@ -439,16 +443,13 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// </summary>
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
         /// <param name="e">The e.</param>
-        void progress_ProgressChanged(object sender, double e)
+        private void OnProgressChanged(object sender, double e)
         {
         {
             e = Math.Min(e, 100);
             e = Math.Min(e, 100);
 
 
             CurrentProgress = e;
             CurrentProgress = e;
 
 
-            TaskProgress?.Invoke(this, new GenericEventArgs<double>
-            {
-                Argument = e
-            });
+            TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -576,6 +577,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="startTime">The start time.</param>
         /// <param name="startTime">The start time.</param>
         /// <param name="endTime">The end time.</param>
         /// <param name="endTime">The end time.</param>
         /// <param name="status">The status.</param>
         /// <param name="status">The status.</param>
+        /// <param name="ex">The exception.</param>
         private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
         private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
         {
         {
             var elapsedTime = endTime - startTime;
             var elapsedTime = endTime - startTime;

+ 5 - 10
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -199,7 +201,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="tasks">The tasks.</param>
         /// <param name="tasks">The tasks.</param>
         public void AddTasks(IEnumerable<IScheduledTask> tasks)
         public void AddTasks(IEnumerable<IScheduledTask> tasks)
         {
         {
-            var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger, _fileSystem));
+            var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger));
 
 
             ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
             ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
         }
         }
@@ -240,10 +242,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="task">The task.</param>
         /// <param name="task">The task.</param>
         internal void OnTaskExecuting(IScheduledTaskWorker task)
         internal void OnTaskExecuting(IScheduledTaskWorker task)
         {
         {
-            TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>
-            {
-                Argument = task
-            });
+            TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -253,11 +252,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="result">The result.</param>
         /// <param name="result">The result.</param>
         internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
         internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
         {
         {
-            TaskCompleted?.Invoke(task, new TaskCompletionEventArgs
-            {
-                Result = result,
-                Task = task
-            });
+            TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result));
 
 
             ExecuteQueuedTasks();
             ExecuteQueuedTasks();
         }
         }

+ 7 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -169,18 +169,25 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
         public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
 
 
+        /// <inheritdoc />
         public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
         public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
 
 
+        /// <inheritdoc />
         public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
         public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
 
 
+        /// <inheritdoc />
         public string Key => "RefreshChapterImages";
         public string Key => "RefreshChapterImages";
 
 
+        /// <inheritdoc />
         public bool IsHidden => false;
         public bool IsHidden => false;
 
 
+        /// <inheritdoc />
         public bool IsEnabled => true;
         public bool IsEnabled => true;
 
 
+        /// <inheritdoc />
         public bool IsLogged => true;
         public bool IsLogged => true;
     }
     }
 }
 }

+ 7 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -165,18 +165,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public string Name => _localization.GetLocalizedString("TaskCleanCache");
         public string Name => _localization.GetLocalizedString("TaskCleanCache");
 
 
+        /// <inheritdoc />
         public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
         public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
 
 
+        /// <inheritdoc />
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
 
 
+        /// <inheritdoc />
         public string Key => "DeleteCacheFiles";
         public string Key => "DeleteCacheFiles";
 
 
+        /// <inheritdoc />
         public bool IsHidden => false;
         public bool IsHidden => false;
 
 
+        /// <inheritdoc />
         public bool IsEnabled => true;
         public bool IsEnabled => true;
 
 
+        /// <inheritdoc />
         public bool IsLogged => true;
         public bool IsLogged => true;
     }
     }
 }
 }

+ 9 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs

@@ -28,6 +28,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
         /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
         /// </summary>
         /// </summary>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="localization">The localization manager.</param>
         public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
         public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
         {
         {
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
@@ -82,18 +84,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
+        /// <inheritdoc />
         public string Name => _localization.GetLocalizedString("TaskCleanLogs");
         public string Name => _localization.GetLocalizedString("TaskCleanLogs");
 
 
+        /// <inheritdoc />
         public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
         public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
 
 
+        /// <inheritdoc />
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
 
 
+        /// <inheritdoc />
         public string Key => "CleanLogFiles";
         public string Key => "CleanLogFiles";
 
 
+        /// <inheritdoc />
         public bool IsHidden => false;
         public bool IsHidden => false;
 
 
+        /// <inheritdoc />
         public bool IsEnabled => true;
         public bool IsEnabled => true;
 
 
+        /// <inheritdoc />
         public bool IsLogged => true;
         public bool IsLogged => true;
     }
     }
 }
 }

+ 7 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs

@@ -132,18 +132,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
         public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
 
 
+        /// <inheritdoc />
         public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
         public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
 
 
+        /// <inheritdoc />
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
         public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
 
 
+        /// <inheritdoc />
         public string Key => "DeleteTranscodeFiles";
         public string Key => "DeleteTranscodeFiles";
 
 
+        /// <inheritdoc />
         public bool IsHidden => false;
         public bool IsHidden => false;
 
 
+        /// <inheritdoc />
         public bool IsEnabled => false;
         public bool IsEnabled => false;
 
 
+        /// <inheritdoc />
         public bool IsLogged => true;
         public bool IsLogged => true;
     }
     }
 }
 }

+ 4 - 6
Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs

@@ -1,8 +1,9 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
@@ -18,19 +19,16 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// The library manager.
         /// The library manager.
         /// </summary>
         /// </summary>
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-
-        private readonly IServerApplicationHost _appHost;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
         /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
         /// </summary>
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="appHost">The server application host</param>
-        public PeopleValidationTask(ILibraryManager libraryManager, IServerApplicationHost appHost, ILocalizationManager localization)
+        /// <param name="localization">The localization manager.</param>
+        public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _appHost = appHost;
             _localization = localization;
             _localization = localization;
         }
         }
 
 

+ 2 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

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

+ 12 - 5
Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs

@@ -1,9 +1,10 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
@@ -16,20 +17,19 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class RefreshMediaLibraryTask : IScheduledTask
     public class RefreshMediaLibraryTask : IScheduledTask
     {
     {
         /// <summary>
         /// <summary>
-        /// The _library manager
+        /// The _library manager.
         /// </summary>
         /// </summary>
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly IServerConfigurationManager _config;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
         /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
         /// </summary>
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config, ILocalizationManager localization)
+        /// <param name="localization">The localization manager.</param>
+        public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _config = config;
             _localization = localization;
             _localization = localization;
         }
         }
 
 
@@ -61,18 +61,25 @@ namespace Emby.Server.Implementations.ScheduledTasks
             return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
             return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc />
         public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
         public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
 
 
+        /// <inheritdoc />
         public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
         public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
 
 
+        /// <inheritdoc />
         public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
         public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
 
 
+        /// <inheritdoc />
         public string Key => "RefreshLibrary";
         public string Key => "RefreshLibrary";
 
 
+        /// <inheritdoc />
         public bool IsHidden => false;
         public bool IsHidden => false;
 
 
+        /// <inheritdoc />
         public bool IsEnabled => true;
         public bool IsEnabled => true;
 
 
+        /// <inheritdoc />
         public bool IsLogged => true;
         public bool IsLogged => true;
     }
     }
 }
 }

+ 3 - 4
Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs

@@ -31,6 +31,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Stars waiting for the trigger action
         /// Stars waiting for the trigger action
         /// </summary>
         /// </summary>
         /// <param name="lastResult">The last result.</param>
         /// <param name="lastResult">The last result.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="taskName">The name of the task.</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         {
         {
@@ -77,10 +79,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// </summary>
         /// </summary>
         private void OnTriggered()
         private void OnTriggered()
         {
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
         }
     }
     }
 }
 }

+ 2 - 0
Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs

@@ -34,6 +34,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Stars waiting for the trigger action
         /// Stars waiting for the trigger action
         /// </summary>
         /// </summary>
         /// <param name="lastResult">The last result.</param>
         /// <param name="lastResult">The last result.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="taskName">The name of the task.</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         {
         {

+ 5 - 1
Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
@@ -6,7 +8,7 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.ScheduledTasks
 namespace Emby.Server.Implementations.ScheduledTasks
 {
 {
     /// <summary>
     /// <summary>
-    /// Class StartupTaskTrigger
+    /// Class StartupTaskTrigger.
     /// </summary>
     /// </summary>
     public class StartupTrigger : ITaskTrigger
     public class StartupTrigger : ITaskTrigger
     {
     {
@@ -26,6 +28,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Stars waiting for the trigger action
         /// Stars waiting for the trigger action
         /// </summary>
         /// </summary>
         /// <param name="lastResult">The last result.</param>
         /// <param name="lastResult">The last result.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="taskName">The name of the task.</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         {
         {

+ 2 - 0
Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs

@@ -37,6 +37,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Stars waiting for the trigger action
         /// Stars waiting for the trigger action
         /// </summary>
         /// </summary>
         /// <param name="lastResult">The last result.</param>
         /// <param name="lastResult">The last result.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="taskName">The name of the task.</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
         {
         {

+ 2 - 0
Emby.Server.Implementations/Security/AuthenticationRepository.cs

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

+ 5 - 0
Emby.Server.Implementations/Serialization/JsonSerializer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -11,6 +13,9 @@ namespace Emby.Server.Implementations.Serialization
     /// </summary>
     /// </summary>
     public class JsonSerializer : IJsonSerializer
     public class JsonSerializer : IJsonSerializer
     {
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="JsonSerializer" /> class.
+        /// </summary>
         public JsonSerializer()
         public JsonSerializer()
         {
         {
             ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
             ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;

+ 2 - 0
Emby.Server.Implementations/Services/HttpResult.cs

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

+ 3 - 4
Emby.Server.Implementations/Services/RequestHelper.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -43,10 +45,7 @@ namespace Emby.Server.Implementations.Services
 
 
         private static string GetContentTypeWithoutEncoding(string contentType)
         private static string GetContentTypeWithoutEncoding(string contentType)
         {
         {
-            return contentType == null
-                       ? null
-                       : contentType.Split(';')[0].ToLowerInvariant().Trim();
+            return contentType?.Split(';')[0].ToLowerInvariant().Trim();
         }
         }
-
     }
     }
 }
 }

+ 3 - 2
Emby.Server.Implementations/Services/ResponseHelper.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -43,8 +45,7 @@ namespace Emby.Server.Implementations.Services
                 response.StatusCode = httpResult.Status;
                 response.StatusCode = httpResult.Status;
             }
             }
 
 
-            var responseOptions = result as IHasHeaders;
-            if (responseOptions != null)
+            if (result is IHasHeaders responseOptions)
             {
             {
                 foreach (var responseHeaders in responseOptions.Headers)
                 foreach (var responseHeaders in responseOptions.Headers)
                 {
                 {

+ 2 - 0
Emby.Server.Implementations/Services/ServiceController.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/ServiceExec.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/ServiceHandler.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/ServiceMethod.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 
 
 namespace Emby.Server.Implementations.Services
 namespace Emby.Server.Implementations.Services

+ 2 - 0
Emby.Server.Implementations/Services/ServicePath.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/SwaggerService.cs

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

+ 2 - 0
Emby.Server.Implementations/Services/UrlExtensions.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 
 

+ 19 - 0
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -25,6 +27,7 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Session
 namespace Emby.Server.Implementations.Session
@@ -1153,6 +1156,22 @@ namespace Emby.Server.Implementations.Session
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
+        /// <inheritdoc />
+        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            var session = GetSessionToRemoteControl(sessionId);
+            await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+        }
+
+        /// <inheritdoc />
+        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            var session = GetSessionToRemoteControl(sessionId);
+            await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+        }
+
         private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);

+ 244 - 1
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -1,8 +1,13 @@
 using System;
 using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -13,6 +18,21 @@ namespace Emby.Server.Implementations.Session
     /// </summary>
     /// </summary>
     public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
     public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
     {
     {
+        /// <summary>
+        /// The timeout in seconds after which a WebSocket is considered to be lost.
+        /// </summary>
+        public const int WebSocketLostTimeout = 60;
+
+        /// <summary>
+        /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
+        /// </summary>
+        public const float IntervalFactor = 0.2f;
+
+        /// <summary>
+        /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
+        /// </summary>
+        public const float ForceKeepAliveFactor = 0.75f;
+
         /// <summary>
         /// <summary>
         /// The _session manager
         /// The _session manager
         /// </summary>
         /// </summary>
@@ -26,6 +46,26 @@ namespace Emby.Server.Implementations.Session
 
 
         private readonly IHttpServer _httpServer;
         private readonly IHttpServer _httpServer;
 
 
+        /// <summary>
+        /// The KeepAlive cancellation token.
+        /// </summary>
+        private CancellationTokenSource _keepAliveCancellationToken;
+
+        /// <summary>
+        /// Lock used for accesing the KeepAlive cancellation token.
+        /// </summary>
+        private readonly object _keepAliveLock = new object();
+
+        /// <summary>
+        /// The WebSocket watchlist.
+        /// </summary>
+        private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
+
+        /// <summary>
+        /// Lock used for accesing the WebSockets watchlist.
+        /// </summary>
+        private readonly object _webSocketsLock = new object();
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
         /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
         /// </summary>
         /// </summary>
@@ -47,12 +87,13 @@ namespace Emby.Server.Implementations.Session
             httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
             httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
         }
 
 
-        private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+        private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
         {
         {
             var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
             var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
             if (session != null)
             if (session != null)
             {
             {
                 EnsureController(session, e.Argument);
                 EnsureController(session, e.Argument);
+                await KeepAliveWebSocket(e.Argument);
             }
             }
             else
             else
             {
             {
@@ -81,6 +122,7 @@ namespace Emby.Server.Implementations.Session
         public void Dispose()
         public void Dispose()
         {
         {
             _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
             _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+            StopKeepAlive();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -99,5 +141,206 @@ namespace Emby.Server.Implementations.Session
             var controller = (WebSocketController)controllerInfo.Item1;
             var controller = (WebSocketController)controllerInfo.Item1;
             controller.AddWebSocket(connection);
             controller.AddWebSocket(connection);
         }
         }
+
+        /// <summary>
+        /// Called when a WebSocket is closed.
+        /// </summary>
+        /// <param name="sender">The WebSocket.</param>
+        /// <param name="e">The event arguments.</param>
+        private void OnWebSocketClosed(object sender, EventArgs e)
+        {
+            var webSocket = (IWebSocketConnection)sender;
+            _logger.LogDebug("WebSocket {0} is closed.", webSocket);
+            RemoveWebSocket(webSocket);
+        }
+
+        /// <summary>
+        /// Adds a WebSocket to the KeepAlive watchlist.
+        /// </summary>
+        /// <param name="webSocket">The WebSocket to monitor.</param>
+        private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
+        {
+            lock (_webSocketsLock)
+            {
+                if (!_webSockets.Add(webSocket))
+                {
+                    _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
+                    return;
+                }
+                webSocket.Closed += OnWebSocketClosed;
+                webSocket.LastKeepAliveDate = DateTime.UtcNow;
+
+                StartKeepAlive();
+            }
+
+            // Notify WebSocket about timeout
+            try
+            {
+                await SendForceKeepAlive(webSocket);
+            }
+            catch (WebSocketException exception)
+            {
+                _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
+            }
+        }
+
+        /// <summary>
+        /// Removes a WebSocket from the KeepAlive watchlist.
+        /// </summary>
+        /// <param name="webSocket">The WebSocket to remove.</param>
+        private void RemoveWebSocket(IWebSocketConnection webSocket)
+        {
+            lock (_webSocketsLock)
+            {
+                if (!_webSockets.Remove(webSocket))
+                {
+                    _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
+                }
+                else
+                {
+                    webSocket.Closed -= OnWebSocketClosed;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Starts the KeepAlive watcher.
+        /// </summary>
+        private void StartKeepAlive()
+        {
+            lock (_keepAliveLock)
+            {
+                if (_keepAliveCancellationToken == null)
+                {
+                    _keepAliveCancellationToken = new CancellationTokenSource();
+                    // Start KeepAlive watcher
+                    _ = RepeatAsyncCallbackEvery(
+                        KeepAliveSockets,
+                        TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
+                        _keepAliveCancellationToken.Token);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Stops the KeepAlive watcher.
+        /// </summary>
+        private void StopKeepAlive()
+        {
+            lock (_keepAliveLock)
+            {
+                if (_keepAliveCancellationToken != null)
+                {
+                    _keepAliveCancellationToken.Cancel();
+                    _keepAliveCancellationToken = null;
+                }
+            }
+
+            lock (_webSocketsLock)
+            {
+                foreach (var webSocket in _webSockets)
+                {
+                    webSocket.Closed -= OnWebSocketClosed;
+                }
+
+                _webSockets.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Checks status of KeepAlive of WebSockets.
+        /// </summary>
+        private async Task KeepAliveSockets()
+        {
+            List<IWebSocketConnection> inactive;
+            List<IWebSocketConnection> lost;
+
+            lock (_webSocketsLock)
+            {
+                _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
+
+                inactive = _webSockets.Where(i =>
+                {
+                    var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
+                    return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
+                }).ToList();
+                lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
+            }
+
+            if (inactive.Any())
+            {
+                _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
+            }
+
+            foreach (var webSocket in inactive)
+            {
+                try
+                {
+                    await SendForceKeepAlive(webSocket);
+                }
+                catch (WebSocketException exception)
+                {
+                    _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
+                    lost.Add(webSocket);
+                }
+            }
+
+            lock (_webSocketsLock)
+            {
+                if (lost.Any())
+                {
+                    _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
+                    foreach (var webSocket in lost)
+                    {
+                        // TODO: handle session relative to the lost webSocket
+                        RemoveWebSocket(webSocket);
+                    }
+                }
+
+                if (!_webSockets.Any())
+                {
+                    StopKeepAlive();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Sends a ForceKeepAlive message to a WebSocket.
+        /// </summary>
+        /// <param name="webSocket">The WebSocket.</param>
+        /// <returns>Task.</returns>
+        private Task SendForceKeepAlive(IWebSocketConnection webSocket)
+        {
+            return webSocket.SendAsync(new WebSocketMessage<int>
+            {
+                MessageType = "ForceKeepAlive",
+                Data = WebSocketLostTimeout
+            }, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Runs a given async callback once every specified interval time, until cancelled.
+        /// </summary>
+        /// <param name="callback">The async callback.</param>
+        /// <param name="interval">The interval time.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                await callback();
+                Task task = Task.Delay(interval, cancellationToken);
+
+                try
+                {
+                    await task;
+                }
+                catch (TaskCanceledException)
+                {
+                    return;
+                }
+            }
+        }
     }
     }
 }
 }

+ 0 - 18
Emby.Server.Implementations/SocketSharp/HttpFile.cs

@@ -1,18 +0,0 @@
-using System.IO;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class HttpFile : IHttpFile
-    {
-        public string Name { get; set; }
-
-        public string FileName { get; set; }
-
-        public long ContentLength { get; set; }
-
-        public string ContentType { get; set; }
-
-        public Stream InputStream { get; set; }
-    }
-}

+ 0 - 198
Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs

@@ -1,198 +0,0 @@
-using System;
-using System.IO;
-
-public sealed class HttpPostedFile : IDisposable
-{
-    private string _name;
-    private string _contentType;
-    private Stream _stream;
-    private bool _disposed = false;
-
-    internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length)
-    {
-        _name = name;
-        _contentType = content_type;
-        _stream = new ReadSubStream(base_stream, offset, length);
-    }
-
-    public string ContentType => _contentType;
-
-    public int ContentLength => (int)_stream.Length;
-
-    public string FileName => _name;
-
-    public Stream InputStream => _stream;
-
-    /// <summary>
-    /// Releases the unmanaged resources and disposes of the managed resources used.
-    /// </summary>
-    public void Dispose()
-    {
-        if (_disposed)
-        {
-            return;
-        }
-
-        _stream.Dispose();
-        _stream = null;
-
-        _name = null;
-        _contentType = null;
-
-        _disposed = true;
-    }
-
-    private class ReadSubStream : Stream
-    {
-        private Stream _stream;
-        private long _offset;
-        private long _end;
-        private long _position;
-
-        public ReadSubStream(Stream s, long offset, long length)
-        {
-            _stream = s;
-            _offset = offset;
-            _end = offset + length;
-            _position = offset;
-        }
-
-        public override bool CanRead => true;
-
-        public override bool CanSeek => true;
-
-        public override bool CanWrite => false;
-
-        public override long Length => _end - _offset;
-
-        public override long Position
-        {
-            get => _position - _offset;
-            set
-            {
-                if (value > Length)
-                {
-                    throw new ArgumentOutOfRangeException(nameof(value));
-                }
-
-                _position = Seek(value, SeekOrigin.Begin);
-            }
-        }
-
-        public override void Flush()
-        {
-        }
-
-        public override int Read(byte[] buffer, int dest_offset, int count)
-        {
-            if (buffer == null)
-            {
-                throw new ArgumentNullException(nameof(buffer));
-            }
-
-            if (dest_offset < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0");
-            }
-
-            if (count < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(count), "< 0");
-            }
-
-            int len = buffer.Length;
-            if (dest_offset > len)
-            {
-                throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset));
-            }
-
-            // reordered to avoid possible integer overflow
-            if (dest_offset > len - count)
-            {
-                throw new ArgumentException("Reading would overrun buffer", nameof(count));
-            }
-
-            if (count > _end - _position)
-            {
-                count = (int)(_end - _position);
-            }
-
-            if (count <= 0)
-            {
-                return 0;
-            }
-
-            _stream.Position = _position;
-            int result = _stream.Read(buffer, dest_offset, count);
-            if (result > 0)
-            {
-                _position += result;
-            }
-            else
-            {
-                _position = _end;
-            }
-
-            return result;
-        }
-
-        public override int ReadByte()
-        {
-            if (_position >= _end)
-            {
-                return -1;
-            }
-
-            _stream.Position = _position;
-            int result = _stream.ReadByte();
-            if (result < 0)
-            {
-                _position = _end;
-            }
-            else
-            {
-                _position++;
-            }
-
-            return result;
-        }
-
-        public override long Seek(long d, SeekOrigin origin)
-        {
-            long real;
-            switch (origin)
-            {
-                case SeekOrigin.Begin:
-                    real = _offset + d;
-                    break;
-                case SeekOrigin.End:
-                    real = _end + d;
-                    break;
-                case SeekOrigin.Current:
-                    real = _position + d;
-                    break;
-                default:
-                    throw new ArgumentException("Unknown SeekOrigin value", nameof(origin));
-            }
-
-            long virt = real - _offset;
-            if (virt < 0 || virt > Length)
-            {
-                throw new ArgumentException("Invalid position", nameof(d));
-            }
-
-            _position = _stream.Seek(real, SeekOrigin.Begin);
-            return _position;
-        }
-
-        public override void SetLength(long value)
-        {
-            throw new NotSupportedException();
-        }
-
-        public override void Write(byte[] buffer, int offset, int count)
-        {
-            throw new NotSupportedException();
-        }
-    }
-}

+ 3 - 1
Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -209,7 +211,7 @@ namespace Emby.Server.Implementations.SocketSharp
         private static string GetQueryStringContentType(HttpRequest httpReq)
         private static string GetQueryStringContentType(HttpRequest httpReq)
         {
         {
             ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
             ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
-            if (format == null)
+            if (format == ReadOnlySpan<char>.Empty)
             {
             {
                 const int FormatMaxLength = 4;
                 const int FormatMaxLength = 4;
                 ReadOnlySpan<char> pi = httpReq.Path.ToString();
                 ReadOnlySpan<char> pi = httpReq.Path.ToString();

+ 2 - 0
Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;

+ 12 - 6
Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -7,6 +9,12 @@ namespace Emby.Server.Implementations.Sorting
 {
 {
     public class CommunityRatingComparer : IBaseItemComparer
     public class CommunityRatingComparer : IBaseItemComparer
     {
     {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.CommunityRating;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -16,18 +24,16 @@ namespace Emby.Server.Implementations.Sorting
         public int Compare(BaseItem x, BaseItem y)
         public int Compare(BaseItem x, BaseItem y)
         {
         {
             if (x == null)
             if (x == null)
+            {
                 throw new ArgumentNullException(nameof(x));
                 throw new ArgumentNullException(nameof(x));
+            }
 
 
             if (y == null)
             if (y == null)
+            {
                 throw new ArgumentNullException(nameof(y));
                 throw new ArgumentNullException(nameof(y));
+            }
 
 
             return (x.CommunityRating ?? 0).CompareTo(y.CommunityRating ?? 0);
             return (x.CommunityRating ?? 0).CompareTo(y.CommunityRating ?? 0);
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.CommunityRating;
     }
     }
 }
 }

+ 9 - 9
Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -26,6 +28,12 @@ namespace Emby.Server.Implementations.Sorting
         /// <value>The user data repository.</value>
         /// <value>The user data repository.</value>
         public IUserDataManager UserDataRepository { get; set; }
         public IUserDataManager UserDataRepository { get; set; }
 
 
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.DateLastContentAdded;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -44,9 +52,7 @@ namespace Emby.Server.Implementations.Sorting
         /// <returns>DateTime.</returns>
         /// <returns>DateTime.</returns>
         private static DateTime GetDate(BaseItem x)
         private static DateTime GetDate(BaseItem x)
         {
         {
-            var folder = x as Folder;
-
-            if (folder != null)
+            if (x is Folder folder)
             {
             {
                 if (folder.DateLastMediaAdded.HasValue)
                 if (folder.DateLastMediaAdded.HasValue)
                 {
                 {
@@ -56,11 +62,5 @@ namespace Emby.Server.Implementations.Sorting
 
 
             return DateTime.MinValue;
             return DateTime.MinValue;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.DateLastContentAdded;
     }
     }
 }
 }

+ 20 - 18
Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -13,6 +15,24 @@ namespace Emby.Server.Implementations.Sorting
         /// <value>The user.</value>
         /// <value>The user.</value>
         public User User { get; set; }
         public User User { get; set; }
 
 
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.IsFavoriteOrLiked;
+
+        /// <summary>
+        /// Gets or sets the user data repository.
+        /// </summary>
+        /// <value>The user data repository.</value>
+        public IUserDataManager UserDataRepository { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user manager.
+        /// </summary>
+        /// <value>The user manager.</value>
+        public IUserManager UserManager { get; set; }
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting
         {
         {
             return x.IsFavoriteOrLiked(User) ? 0 : 1;
             return x.IsFavoriteOrLiked(User) ? 0 : 1;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.IsFavoriteOrLiked;
-
-        /// <summary>
-        /// Gets or sets the user data repository.
-        /// </summary>
-        /// <value>The user data repository.</value>
-        public IUserDataManager UserDataRepository { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
     }
     }
 }
 }

+ 8 - 6
Emby.Server.Implementations/Sorting/IsFolderComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
@@ -6,6 +8,12 @@ namespace Emby.Server.Implementations.Sorting
 {
 {
     public class IsFolderComparer : IBaseItemComparer
     public class IsFolderComparer : IBaseItemComparer
     {
     {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.IsFolder;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -26,11 +34,5 @@ namespace Emby.Server.Implementations.Sorting
         {
         {
             return x.IsFolder ? 0 : 1;
             return x.IsFolder ? 0 : 1;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.IsFolder;
     }
     }
 }
 }

+ 20 - 18
Emby.Server.Implementations/Sorting/IsPlayedComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -13,6 +15,24 @@ namespace Emby.Server.Implementations.Sorting
         /// <value>The user.</value>
         /// <value>The user.</value>
         public User User { get; set; }
         public User User { get; set; }
 
 
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.IsUnplayed;
+
+        /// <summary>
+        /// Gets or sets the user data repository.
+        /// </summary>
+        /// <value>The user data repository.</value>
+        public IUserDataManager UserDataRepository { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user manager.
+        /// </summary>
+        /// <value>The user manager.</value>
+        public IUserManager UserManager { get; set; }
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting
         {
         {
             return x.IsPlayed(User) ? 0 : 1;
             return x.IsPlayed(User) ? 0 : 1;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.IsUnplayed;
-
-        /// <summary>
-        /// Gets or sets the user data repository.
-        /// </summary>
-        /// <value>The user data repository.</value>
-        public IUserDataManager UserDataRepository { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
     }
     }
 }
 }

+ 20 - 18
Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -13,6 +15,24 @@ namespace Emby.Server.Implementations.Sorting
         /// <value>The user.</value>
         /// <value>The user.</value>
         public User User { get; set; }
         public User User { get; set; }
 
 
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.IsUnplayed;
+
+        /// <summary>
+        /// Gets or sets the user data repository.
+        /// </summary>
+        /// <value>The user data repository.</value>
+        public IUserDataManager UserDataRepository { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user manager.
+        /// </summary>
+        /// <value>The user manager.</value>
+        public IUserManager UserManager { get; set; }
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting
         {
         {
             return x.IsUnplayed(User) ? 0 : 1;
             return x.IsUnplayed(User) ? 0 : 1;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.IsUnplayed;
-
-        /// <summary>
-        /// Gets or sets the user data repository.
-        /// </summary>
-        /// <value>The user data repository.</value>
-        public IUserDataManager UserDataRepository { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
     }
     }
 }
 }

+ 8 - 6
Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -15,6 +17,12 @@ namespace Emby.Server.Implementations.Sorting
             _localization = localization;
             _localization = localization;
         }
         }
 
 
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.OfficialRating;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -38,11 +46,5 @@ namespace Emby.Server.Implementations.Sorting
 
 
             return levelX.CompareTo(levelY);
             return levelX.CompareTo(levelY);
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.OfficialRating;
     }
     }
 }
 }

+ 8 - 6
Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
@@ -7,6 +9,12 @@ namespace Emby.Server.Implementations.Sorting
 {
 {
     public class SeriesSortNameComparer : IBaseItemComparer
     public class SeriesSortNameComparer : IBaseItemComparer
     {
     {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.SeriesSortName;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -18,12 +26,6 @@ namespace Emby.Server.Implementations.Sorting
             return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
             return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
         }
         }
 
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.SeriesSortName;
-
         private static string GetValue(BaseItem item)
         private static string GetValue(BaseItem item)
         {
         {
             var hasSeries = item as IHasSeries;
             var hasSeries = item as IHasSeries;

+ 10 - 9
Emby.Server.Implementations/Sorting/StartDateComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
@@ -8,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
 {
 {
     public class StartDateComparer : IBaseItemComparer
     public class StartDateComparer : IBaseItemComparer
     {
     {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name => ItemSortBy.StartDate;
+
         /// <summary>
         /// <summary>
         /// Compares the specified x.
         /// Compares the specified x.
         /// </summary>
         /// </summary>
@@ -26,19 +34,12 @@ namespace Emby.Server.Implementations.Sorting
         /// <returns>DateTime.</returns>
         /// <returns>DateTime.</returns>
         private static DateTime GetDate(BaseItem x)
         private static DateTime GetDate(BaseItem x)
         {
         {
-            var hasStartDate = x as LiveTvProgram;
-
-            if (hasStartDate != null)
+            if (x is LiveTvProgram hasStartDate)
             {
             {
                 return hasStartDate.StartDate;
                 return hasStartDate.StartDate;
             }
             }
+
             return DateTime.MinValue;
             return DateTime.MinValue;
         }
         }
-
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => ItemSortBy.StartDate;
     }
     }
 }
 }

+ 2 - 0
Emby.Server.Implementations/Sorting/StudioComparer.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;

+ 522 - 0
Emby.Server.Implementations/SyncPlay/SyncPlayController.cs

@@ -0,0 +1,522 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace Emby.Server.Implementations.SyncPlay
+{
+    /// <summary>
+    /// Class SyncPlayController.
+    /// </summary>
+    /// <remarks>
+    /// Class is not thread-safe, external locking is required when accessing methods.
+    /// </remarks>
+    public class SyncPlayController : ISyncPlayController
+    {
+        /// <summary>
+        /// Used to filter the sessions of a group.
+        /// </summary>
+        private enum BroadcastType
+        {
+            /// <summary>
+            /// All sessions will receive the message.
+            /// </summary>
+            AllGroup = 0,
+            /// <summary>
+            /// Only the specified session will receive the message.
+            /// </summary>
+            CurrentSession = 1,
+            /// <summary>
+            /// All sessions, except the current one, will receive the message.
+            /// </summary>
+            AllExceptCurrentSession = 2,
+            /// <summary>
+            /// Only sessions that are not buffering will receive the message.
+            /// </summary>
+            AllReady = 3
+        }
+
+        /// <summary>
+        /// The session manager.
+        /// </summary>
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// The SyncPlay manager.
+        /// </summary>
+        private readonly ISyncPlayManager _syncPlayManager;
+
+        /// <summary>
+        /// The group to manage.
+        /// </summary>
+        private readonly GroupInfo _group = new GroupInfo();
+
+        /// <inheritdoc />
+        public Guid GetGroupId() => _group.GroupId;
+
+        /// <inheritdoc />
+        public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+        /// <inheritdoc />
+        public bool IsGroupEmpty() => _group.IsEmpty();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SyncPlayController" /> class.
+        /// </summary>
+        /// <param name="sessionManager">The session manager.</param>
+        /// <param name="syncPlayManager">The SyncPlay manager.</param>
+        public SyncPlayController(
+            ISessionManager sessionManager,
+            ISyncPlayManager syncPlayManager)
+        {
+            _sessionManager = sessionManager;
+            _syncPlayManager = syncPlayManager;
+        }
+
+        /// <summary>
+        /// Converts DateTime to UTC string.
+        /// </summary>
+        /// <param name="date">The date to convert.</param>
+        /// <value>The UTC string.</value>
+        private string DateToUTCString(DateTime date)
+        {
+            return date.ToUniversalTime().ToString("o");
+        }
+
+        /// <summary>
+        /// Filters sessions of this group.
+        /// </summary>
+        /// <param name="from">The current session.</param>
+        /// <param name="type">The filtering type.</param>
+        /// <value>The array of sessions matching the filter.</value>
+        private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
+        {
+            switch (type)
+            {
+                case BroadcastType.CurrentSession:
+                    return new SessionInfo[] { from };
+                case BroadcastType.AllGroup:
+                    return _group.Participants.Values.Select(
+                        session => session.Session
+                    ).ToArray();
+                case BroadcastType.AllExceptCurrentSession:
+                    return _group.Participants.Values.Select(
+                        session => session.Session
+                    ).Where(
+                        session => !session.Id.Equals(from.Id)
+                    ).ToArray();
+                case BroadcastType.AllReady:
+                    return _group.Participants.Values.Where(
+                        session => !session.IsBuffering
+                    ).Select(
+                        session => session.Session
+                    ).ToArray();
+                default:
+                    return Array.Empty<SessionInfo>();
+            }
+        }
+
+        /// <summary>
+        /// Sends a GroupUpdate message to the interested sessions.
+        /// </summary>
+        /// <param name="from">The current session.</param>
+        /// <param name="type">The filtering type.</param>
+        /// <param name="message">The message to send.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <value>The task.</value>
+        private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
+        {
+            IEnumerable<Task> GetTasks()
+            {
+                SessionInfo[] sessions = FilterSessions(from, type);
+                foreach (var session in sessions)
+                {
+                    yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+
+        /// <summary>
+        /// Sends a playback command to the interested sessions.
+        /// </summary>
+        /// <param name="from">The current session.</param>
+        /// <param name="type">The filtering type.</param>
+        /// <param name="message">The message to send.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <value>The task.</value>
+        private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken)
+        {
+            IEnumerable<Task> GetTasks()
+            {
+                SessionInfo[] sessions = FilterSessions(from, type);
+                foreach (var session in sessions)
+                {
+                    yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+
+        /// <summary>
+        /// Builds a new playback command with some default values.
+        /// </summary>
+        /// <param name="type">The command type.</param>
+        /// <value>The SendCommand.</value>
+        private SendCommand NewSyncPlayCommand(SendCommandType type)
+        {
+            return new SendCommand()
+            {
+                GroupId = _group.GroupId.ToString(),
+                Command = type,
+                PositionTicks = _group.PositionTicks,
+                When = DateToUTCString(_group.LastActivity),
+                EmittedAt = DateToUTCString(DateTime.UtcNow)
+            };
+        }
+
+        /// <summary>
+        /// Builds a new group update message.
+        /// </summary>
+        /// <param name="type">The update type.</param>
+        /// <param name="data">The data to send.</param>
+        /// <value>The GroupUpdate.</value>
+        private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
+        {
+            return new GroupUpdate<T>()
+            {
+                GroupId = _group.GroupId.ToString(),
+                Type = type,
+                Data = data
+            };
+        }
+
+        /// <inheritdoc />
+        public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
+        {
+            _group.AddSession(session);
+            _syncPlayManager.AddSessionToGroup(session, this);
+
+            _group.PlayingItem = session.FullNowPlayingItem;
+            _group.IsPaused = true;
+            _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
+            _group.LastActivity = DateTime.UtcNow;
+
+            var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+            var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
+            SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
+        }
+
+        /// <inheritdoc />
+        public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
+        {
+            if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
+            {
+                _group.AddSession(session);
+                _syncPlayManager.AddSessionToGroup(session, this);
+
+                var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+                SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+
+                var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
+                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+
+                // Client join and play, syncing will happen client side
+                if (!_group.IsPaused)
+                {
+                    var playCommand = NewSyncPlayCommand(SendCommandType.Play);
+                    SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken);
+                }
+                else
+                {
+                    var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
+                    SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
+                }
+            }
+            else
+            {
+                var playRequest = new PlayRequest();
+                playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
+                playRequest.StartPositionTicks = _group.PositionTicks;
+                var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
+                SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
+            }
+        }
+
+        /// <inheritdoc />
+        public void SessionLeave(SessionInfo session, CancellationToken cancellationToken)
+        {
+            _group.RemoveSession(session);
+            _syncPlayManager.RemoveSessionFromGroup(session, this);
+
+            var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
+            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+
+            var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
+            SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+        }
+
+        /// <inheritdoc />
+        public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            // The server's job is to mantain a consistent state to which clients refer to,
+            // as also to notify clients of state changes.
+            // The actual syncing of media playback happens client side.
+            // Clients are aware of the server's time and use it to sync.
+            switch (request.Type)
+            {
+                case PlaybackRequestType.Play:
+                    HandlePlayRequest(session, request, cancellationToken);
+                    break;
+                case PlaybackRequestType.Pause:
+                    HandlePauseRequest(session, request, cancellationToken);
+                    break;
+                case PlaybackRequestType.Seek:
+                    HandleSeekRequest(session, request, cancellationToken);
+                    break;
+                case PlaybackRequestType.Buffering:
+                    HandleBufferingRequest(session, request, cancellationToken);
+                    break;
+                case PlaybackRequestType.BufferingDone:
+                    HandleBufferingDoneRequest(session, request, cancellationToken);
+                    break;
+                case PlaybackRequestType.UpdatePing:
+                    HandlePingUpdateRequest(session, request);
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Handles a play action requested by a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The play action.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            if (_group.IsPaused)
+            {
+                // Pick a suitable time that accounts for latency
+                var delay = _group.GetHighestPing() * 2;
+                delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+                // Unpause group and set starting point in future
+                // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
+                // The added delay does not guarantee, of course, that the command will be received in time
+                // Playback synchronization will mainly happen client side
+                _group.IsPaused = false;
+                _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
+                    delay
+                );
+
+                var command = NewSyncPlayCommand(SendCommandType.Play);
+                SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+            }
+            else
+            {
+                // Client got lost, sending current state
+                var command = NewSyncPlayCommand(SendCommandType.Play);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+            }
+        }
+
+        /// <summary>
+        /// Handles a pause action requested by a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The pause action.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            if (!_group.IsPaused)
+            {
+                // Pause group and compute the media playback position
+                _group.IsPaused = true;
+                var currentTime = DateTime.UtcNow;
+                var elapsedTime = currentTime - _group.LastActivity;
+                _group.LastActivity = currentTime;
+                // Seek only if playback actually started
+                // (a pause request may be issued during the delay added to account for latency)
+                _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+                var command = NewSyncPlayCommand(SendCommandType.Pause);
+                SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+            }
+            else
+            {
+                // Client got lost, sending current state
+                var command = NewSyncPlayCommand(SendCommandType.Pause);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+            }
+        }
+
+        /// <summary>
+        /// Handles a seek action requested by a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The seek action.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            // Sanitize PositionTicks
+            var ticks = SanitizePositionTicks(request.PositionTicks);
+
+            // Pause and seek
+            _group.IsPaused = true;
+            _group.PositionTicks = ticks;
+            _group.LastActivity = DateTime.UtcNow;
+
+            var command = NewSyncPlayCommand(SendCommandType.Seek);
+            SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+        }
+
+        /// <summary>
+        /// Handles a buffering action requested by a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The buffering action.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            if (!_group.IsPaused)
+            {
+                // Pause group and compute the media playback position
+                _group.IsPaused = true;
+                var currentTime = DateTime.UtcNow;
+                var elapsedTime = currentTime - _group.LastActivity;
+                _group.LastActivity = currentTime;
+                _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+                _group.SetBuffering(session, true);
+
+                // Send pause command to all non-buffering sessions
+                var command = NewSyncPlayCommand(SendCommandType.Pause);
+                SendCommand(session, BroadcastType.AllReady, command, cancellationToken);
+
+                var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
+                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+            }
+            else
+            {
+                // Client got lost, sending current state
+                var command = NewSyncPlayCommand(SendCommandType.Pause);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+            }
+        }
+
+        /// <summary>
+        /// Handles a buffering-done action requested by a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The buffering-done action.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            if (_group.IsPaused)
+            {
+                _group.SetBuffering(session, false);
+
+                var requestTicks = SanitizePositionTicks(request.PositionTicks);
+
+                var when = request.When ?? DateTime.UtcNow;
+                var currentTime = DateTime.UtcNow;
+                var elapsedTime = currentTime - when;
+                var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
+                var delay = _group.PositionTicks - clientPosition.Ticks;
+
+                if (_group.IsBuffering())
+                {
+                    // Others are still buffering, tell this client to pause when ready
+                    var command = NewSyncPlayCommand(SendCommandType.Pause);
+                    var pauseAtTime = currentTime.AddMilliseconds(delay);
+                    command.When = DateToUTCString(pauseAtTime);
+                    SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+                }
+                else
+                {
+                    // Let other clients resume as soon as the buffering client catches up
+                    _group.IsPaused = false;
+
+                    if (delay > _group.GetHighestPing() * 2)
+                    {
+                        // Client that was buffering is recovering, notifying others to resume
+                        _group.LastActivity = currentTime.AddMilliseconds(
+                            delay
+                        );
+                        var command = NewSyncPlayCommand(SendCommandType.Play);
+                        SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken);
+                    }
+                    else
+                    {
+                        // Client, that was buffering, resumed playback but did not update others in time
+                        delay = _group.GetHighestPing() * 2;
+                        delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+                        _group.LastActivity = currentTime.AddMilliseconds(
+                            delay
+                        );
+
+                        var command = NewSyncPlayCommand(SendCommandType.Play);
+                        SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+                    }
+                }
+            }
+            else
+            {
+                // Group was not waiting, make sure client has latest state
+                var command = NewSyncPlayCommand(SendCommandType.Play);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+            }
+        }
+
+        /// <summary>
+        /// Sanitizes the PositionTicks, considers the current playing item when available.
+        /// </summary>
+        /// <param name="positionTicks">The PositionTicks.</param>
+        /// <value>The sanitized PositionTicks.</value>
+        private long SanitizePositionTicks(long? positionTicks)
+        {
+            var ticks = positionTicks ?? 0;
+            ticks = ticks >= 0 ? ticks : 0;
+            if (_group.PlayingItem != null)
+            {
+                var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
+                ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
+            }
+
+            return ticks;
+        }
+
+        /// <summary>
+        /// Updates ping of a session.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="request">The update.</param>
+        private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
+        {
+            // Collected pings are used to account for network latency when unpausing playback
+            _group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
+        }
+
+        /// <inheritdoc />
+        public GroupInfoView GetInfo()
+        {
+            return new GroupInfoView()
+            {
+                GroupId = GetGroupId().ToString(),
+                PlayingItemName = _group.PlayingItem.Name,
+                PlayingItemId = _group.PlayingItem.Id.ToString(),
+                PositionTicks = _group.PositionTicks,
+                Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToList()
+            };
+        }
+    }
+}

+ 405 - 0
Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs

@@ -0,0 +1,405 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.SyncPlay;
+
+namespace Emby.Server.Implementations.SyncPlay
+{
+    /// <summary>
+    /// Class SyncPlayManager.
+    /// </summary>
+    public class SyncPlayManager : ISyncPlayManager, IDisposable
+    {
+        /// <summary>
+        /// The logger.
+        /// </summary>
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// The user manager.
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The session manager.
+        /// </summary>
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// The library manager.
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The map between sessions and groups.
+        /// </summary>
+        private readonly Dictionary<string, ISyncPlayController> _sessionToGroupMap =
+            new Dictionary<string, ISyncPlayController>(StringComparer.OrdinalIgnoreCase);
+
+        /// <summary>
+        /// The groups.
+        /// </summary>
+        private readonly Dictionary<Guid, ISyncPlayController> _groups =
+            new Dictionary<Guid, ISyncPlayController>();
+
+        /// <summary>
+        /// Lock used for accesing any group.
+        /// </summary>
+        private readonly object _groupsLock = new object();
+
+        private bool _disposed = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="sessionManager">The session manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        public SyncPlayManager(
+            ILogger<SyncPlayManager> logger,
+            IUserManager userManager,
+            ISessionManager sessionManager,
+            ILibraryManager libraryManager)
+        {
+            _logger = logger;
+            _userManager = userManager;
+            _sessionManager = sessionManager;
+            _libraryManager = libraryManager;
+
+            _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+        }
+
+        /// <summary>
+        /// Gets all groups.
+        /// </summary>
+        /// <value>All groups.</value>
+        public IEnumerable<ISyncPlayController> Groups => _groups.Values;
+
+        /// <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)
+            {
+                return;
+            }
+
+            _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+
+            _disposed = true;
+        }
+
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+
+        private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
+        {
+            var session = e.SessionInfo;
+            if (!IsSessionInGroup(session))
+            {
+                return;
+            }
+
+            LeaveGroup(session, CancellationToken.None);
+        }
+
+        private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
+        {
+            var session = e.Session;
+            if (!IsSessionInGroup(session))
+            {
+                return;
+            }
+
+            LeaveGroup(session, CancellationToken.None);
+        }
+
+        private bool IsSessionInGroup(SessionInfo session)
+        {
+            return _sessionToGroupMap.ContainsKey(session.Id);
+        }
+
+        private bool HasAccessToItem(User user, Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+
+            // Check ParentalRating access
+            var hasParentalRatingAccess = true;
+            if (user.Policy.MaxParentalRating.HasValue)
+            {
+                hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
+            }
+
+            if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
+            {
+                var collections = _libraryManager.GetCollectionFolders(item).Select(
+                    folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
+                );
+                var intersect = collections.Intersect(user.Policy.EnabledFolders);
+                return intersect.Any();
+            }
+            else
+            {
+                return hasParentalRatingAccess;
+            }
+        }
+
+        private Guid? GetSessionGroup(SessionInfo session)
+        {
+            ISyncPlayController group;
+            _sessionToGroupMap.TryGetValue(session.Id, out group);
+            if (group != null)
+            {
+                return group.GetGroupId();
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        /// <inheritdoc />
+        public void NewGroup(SessionInfo session, CancellationToken cancellationToken)
+        {
+            var user = _userManager.GetUserById(session.UserId);
+
+            if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
+            {
+                _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
+
+                var error = new GroupUpdate<string>()
+                {
+                    Type = GroupUpdateType.CreateGroupDenied
+                };
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                return;
+            }
+
+            lock (_groupsLock)
+            {
+                if (IsSessionInGroup(session))
+                {
+                    LeaveGroup(session, cancellationToken);
+                }
+
+                var group = new SyncPlayController(_sessionManager, this);
+                _groups[group.GetGroupId()] = group;
+
+                group.InitGroup(session, cancellationToken);
+            }
+        }
+
+        /// <inheritdoc />
+        public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken)
+        {
+            var user = _userManager.GetUserById(session.UserId);
+
+            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            {
+                _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
+
+                var error = new GroupUpdate<string>()
+                {
+                    Type = GroupUpdateType.JoinGroupDenied
+                };
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                return;
+            }
+
+            lock (_groupsLock)
+            {
+                ISyncPlayController group;
+                _groups.TryGetValue(groupId, out group);
+
+                if (group == null)
+                {
+                    _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId);
+
+                    var error = new GroupUpdate<string>()
+                    {
+                        Type = GroupUpdateType.GroupDoesNotExist
+                    };
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    return;
+                }
+
+                if (!HasAccessToItem(user, group.GetPlayingItemId()))
+                {
+                    _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId());
+
+                    var error = new GroupUpdate<string>()
+                    {
+                        GroupId = group.GetGroupId().ToString(),
+                        Type = GroupUpdateType.LibraryAccessDenied
+                    };
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    return;
+                }
+
+                if (IsSessionInGroup(session))
+                {
+                    if (GetSessionGroup(session).Equals(groupId))
+                    {
+                        return;
+                    }
+
+                    LeaveGroup(session, cancellationToken);
+                }
+
+                group.SessionJoin(session, request, cancellationToken);
+            }
+        }
+
+        /// <inheritdoc />
+        public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken)
+        {
+            // TODO: determine what happens to users that are in a group and get their permissions revoked
+            lock (_groupsLock)
+            {
+                ISyncPlayController group;
+                _sessionToGroupMap.TryGetValue(session.Id, out group);
+
+                if (group == null)
+                {
+                    _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id);
+
+                    var error = new GroupUpdate<string>()
+                    {
+                        Type = GroupUpdateType.NotInGroup
+                    };
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    return;
+                }
+
+                group.SessionLeave(session, cancellationToken);
+
+                if (group.IsGroupEmpty())
+                {
+                    _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId());
+                    _groups.Remove(group.GetGroupId(), out _);
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        public List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId)
+        {
+            var user = _userManager.GetUserById(session.UserId);
+
+            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            {
+                return new List<GroupInfoView>();
+            }
+
+            // Filter by item if requested
+            if (!filterItemId.Equals(Guid.Empty))
+            {
+                return _groups.Values.Where(
+                    group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())
+                ).Select(
+                    group => group.GetInfo()
+                ).ToList();
+            }
+            // Otherwise show all available groups
+            else
+            {
+                return _groups.Values.Where(
+                    group => HasAccessToItem(user, group.GetPlayingItemId())
+                ).Select(
+                    group => group.GetInfo()
+                ).ToList();
+            }
+        }
+
+        /// <inheritdoc />
+        public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+        {
+            var user = _userManager.GetUserById(session.UserId);
+
+            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            {
+                _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
+
+                var error = new GroupUpdate<string>()
+                {
+                    Type = GroupUpdateType.JoinGroupDenied
+                };
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                return;
+            }
+
+            lock (_groupsLock)
+            {
+                ISyncPlayController group;
+                _sessionToGroupMap.TryGetValue(session.Id, out group);
+
+                if (group == null)
+                {
+                    _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id);
+
+                    var error = new GroupUpdate<string>()
+                    {
+                        Type = GroupUpdateType.NotInGroup
+                    };
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    return;
+                }
+
+                group.HandleRequest(session, request, cancellationToken);
+            }
+        }
+
+        /// <inheritdoc />
+        public void AddSessionToGroup(SessionInfo session, ISyncPlayController group)
+        {
+            if (IsSessionInGroup(session))
+            {
+                throw new InvalidOperationException("Session in other group already!");
+            }
+
+            _sessionToGroupMap[session.Id] = group;
+        }
+
+        /// <inheritdoc />
+        public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group)
+        {
+            if (!IsSessionInGroup(session))
+            {
+                throw new InvalidOperationException("Session not in any group!");
+            }
+
+            ISyncPlayController tempGroup;
+            _sessionToGroupMap.Remove(session.Id, out tempGroup);
+
+            if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
+            {
+                throw new InvalidOperationException("Session was in wrong group!");
+            }
+        }
+    }
+}

+ 3 - 4
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -1,8 +1,9 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
@@ -18,14 +19,12 @@ namespace Emby.Server.Implementations.TV
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly IServerConfigurationManager _config;
 
 
-        public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager config)
+        public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _config = config;
         }
         }
 
 
         public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
         public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)

+ 13 - 3
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -7,6 +7,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Model.ApiClient;
 using MediaBrowser.Model.ApiClient;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Udp
 namespace Emby.Server.Implementations.Udp
@@ -21,6 +22,12 @@ namespace Emby.Server.Implementations.Udp
         /// </summary>
         /// </summary>
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _config;
+
+        /// <summary>
+        /// Address Override Configuration Key.
+        /// </summary>
+        public const string AddressOverrideConfigKey = "PublishedServerUrl";
 
 
         private Socket _udpSocket;
         private Socket _udpSocket;
         private IPEndPoint _endpoint;
         private IPEndPoint _endpoint;
@@ -31,15 +38,18 @@ namespace Emby.Server.Implementations.Udp
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServer" /> class.
         /// Initializes a new instance of the <see cref="UdpServer" /> class.
         /// </summary>
         /// </summary>
-        public UdpServer(ILogger logger, IServerApplicationHost appHost)
+        public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
         {
         {
             _logger = logger;
             _logger = logger;
             _appHost = appHost;
             _appHost = appHost;
+            _config = configuration;
         }
         }
 
 
         private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
         private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
         {
         {
-            var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+            string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
+                ? _config[AddressOverrideConfigKey]
+                : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
 
 
             if (!string.IsNullOrEmpty(localUrl))
             if (!string.IsNullOrEmpty(localUrl))
             {
             {
@@ -105,7 +115,7 @@ namespace Emby.Server.Implementations.Udp
                 }
                 }
                 catch (SocketException ex)
                 catch (SocketException ex)
                 {
                 {
-                    _logger.LogError(ex, "Failed to receive data drom socket");
+                    _logger.LogError(ex, "Failed to receive data from socket");
                 }
                 }
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {

+ 2 - 2
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -1,12 +1,12 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Net;
 using System.Net.Http;
 using System.Net.Http;
-using System.Runtime.CompilerServices;
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
 using System.Threading;
 using System.Threading;

+ 2 - 0
Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs

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

+ 3 - 3
Emby.Server.Implementations/UserViews/DynamicImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -19,13 +21,11 @@ namespace Emby.Server.Implementations.UserViews
     public class DynamicImageProvider : BaseDynamicImageProvider<UserView>
     public class DynamicImageProvider : BaseDynamicImageProvider<UserView>
     {
     {
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
 
 
-        public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager)
+        public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager)
             : base(fileSystem, providerManager, applicationPaths, imageProcessor)
             : base(fileSystem, providerManager, applicationPaths, imageProcessor)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
-            _libraryManager = libraryManager;
         }
         }
 
 
         protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
         protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)

+ 4 - 2
Emby.Server.Implementations/UserViews/FolderImageProvider.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Emby.Server.Implementations.Images;
 using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -75,14 +77,14 @@ namespace Emby.Server.Implementations.UserViews
                 return false;
                 return false;
             }
             }
 
 
-            var folder = item as Folder;
-            if (folder != null)
+            if (item is Folder folder)
             {
             {
                 if (folder.IsTopParent)
                 if (folder.IsTopParent)
                 {
                 {
                     return false;
                     return false;
                 }
                 }
             }
             }
+
             return true;
             return true;
             //return item.SourceType == SourceType.Library;
             //return item.SourceType == SourceType.Library;
         }
         }

+ 2 - 0
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -18,6 +18,8 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <PackageReference Include="BlurHashSharp" Version="1.0.1" />
+    <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
     <PackageReference Include="SkiaSharp" Version="1.68.1" />
     <PackageReference Include="SkiaSharp" Version="1.68.1" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
     <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
     <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />

+ 15 - 0
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
+using BlurHashSharp.SkiaSharp;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
@@ -229,6 +230,20 @@ namespace Jellyfin.Drawing.Skia
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
+        /// <exception cref="ArgumentNullException">The path is null.</exception>
+        /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+        /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
+        public string GetImageBlurHash(int xComp, int yComp, string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException(nameof(path));
+            }
+
+            return BlurHashEncoder.Encode(xComp, yComp, path);
+        }
+
         private static bool HasDiacritics(string text)
         private static bool HasDiacritics(string text)
             => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
             => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
 
 

+ 0 - 1
Jellyfin.Server/Migrations/IMigrationRoutine.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using Microsoft.Extensions.Logging;
 
 
 namespace Jellyfin.Server.Migrations
 namespace Jellyfin.Server.Migrations
 {
 {

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff