فهرست منبع

Merge branch 'master' into tonemap

Nyanmisaka 4 سال پیش
والد
کامیت
df6b303da7
30فایلهای تغییر یافته به همراه486 افزوده شده و 614 حذف شده
  1. 24 21
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  2. 2 2
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  3. 2 1
      Emby.Server.Implementations/IO/FileRefresher.cs
  4. 20 3
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  5. 15 18
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  6. 6 5
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  7. 1 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  8. 23 20
      Emby.Server.Implementations/Net/UdpSocket.cs
  9. 20 44
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  10. 14 4
      Emby.Server.Implementations/Services/ServiceController.cs
  11. 4 4
      Emby.Server.Implementations/Services/ServiceHandler.cs
  12. 2 2
      Emby.Server.Implementations/Session/SessionManager.cs
  13. 16 14
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  14. 2 2
      Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
  15. 32 28
      Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
  16. 9 13
      Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
  17. 15 19
      Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
  18. 1 1
      Jellyfin.Drawing.Skia/SkiaCodecException.cs
  19. 162 310
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  20. 1 1
      Jellyfin.Drawing.Skia/SkiaException.cs
  21. 51 69
      Jellyfin.Drawing.Skia/StripCollageBuilder.cs
  22. 24 28
      Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
  23. 2 2
      Jellyfin.Server.Implementations/Users/UserManager.cs
  24. 6 0
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  25. 7 1
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  26. 3 0
      MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
  27. 3 0
      MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
  28. 3 0
      MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
  29. 3 0
      MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
  30. 13 1
      tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

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

@@ -2776,82 +2776,82 @@ namespace Emby.Server.Implementations.Data
 
         private string FixUnicodeChars(string buffer)
         {
-            if (buffer.IndexOf('\u2013') > -1)
+            if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2013', '-'); // en dash
             }
 
-            if (buffer.IndexOf('\u2014') > -1)
+            if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2014', '-'); // em dash
             }
 
-            if (buffer.IndexOf('\u2015') > -1)
+            if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2015', '-'); // horizontal bar
             }
 
-            if (buffer.IndexOf('\u2017') > -1)
+            if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2017', '_'); // double low line
             }
 
-            if (buffer.IndexOf('\u2018') > -1)
+            if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
             }
 
-            if (buffer.IndexOf('\u2019') > -1)
+            if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
             }
 
-            if (buffer.IndexOf('\u201a') > -1)
+            if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
             }
 
-            if (buffer.IndexOf('\u201b') > -1)
+            if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
             }
 
-            if (buffer.IndexOf('\u201c') > -1)
+            if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
             }
 
-            if (buffer.IndexOf('\u201d') > -1)
+            if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
             }
 
-            if (buffer.IndexOf('\u201e') > -1)
+            if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
             }
 
-            if (buffer.IndexOf('\u2026') > -1)
+            if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
             {
-                buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
+                buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
             }
 
-            if (buffer.IndexOf('\u2032') > -1)
+            if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2032', '\''); // prime
             }
 
-            if (buffer.IndexOf('\u2033') > -1)
+            if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u2033', '\"'); // double prime
             }
 
-            if (buffer.IndexOf('\u0060') > -1)
+            if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u0060', '\''); // grave accent
             }
 
-            if (buffer.IndexOf('\u00B4') > -1)
+            if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
             {
                 buffer = buffer.Replace('\u00B4', '\''); // acute accent
             }
@@ -3000,7 +3000,6 @@ namespace Emby.Server.Implementations.Data
             {
                 connection.RunInTransaction(db =>
                 {
-
                     var statements = PrepareAll(db, statementTexts).ToList();
 
                     if (!isReturningZeroItems)
@@ -4670,8 +4669,12 @@ namespace Emby.Server.Implementations.Data
 
             if (query.BlockUnratedItems.Length > 1)
             {
-                var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
-                whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
+                var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
+                whereClauses.Add(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
+                        inClause));
             }
 
             if (query.ExcludeInheritedTags.Length > 0)
@@ -4680,7 +4683,7 @@ namespace Emby.Server.Implementations.Data
                 if (statement == null)
                 {
                     int index = 0;
-                    string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
+                    string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
                     whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
                 }
                 else

+ 2 - 2
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
                 if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
                 {
                     httpRes.StatusCode = 200;
-                    foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
                     {
                         httpRes.Headers.Add(key, value);
                     }
@@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
                 var handler = GetServiceHandler(httpReq);
                 if (handler != null)
                 {
-                    await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
+                    await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
                 }
                 else
                 {

+ 2 - 1
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly object _timerLock = new object();
         private Timer _timer;
+        private bool _disposed;
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         {
@@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        private bool _disposed;
         public void Dispose()
         {
             _disposed = true;
             DisposeTimer();
+            GC.SuppressFinalize(this);
         }
     }
 }

+ 20 - 3
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
         {
             "**/small.jpg",
             "**/albumart.jpg",
-            "**/*sample*",
+
+            // We have neither non-greedy matching or character group repetitions, working around that here.
+            // https://github.com/dazinator/DotNet.Glob#patterns
+            // .*/sample\..{1,5}
+            "**/sample.?",
+            "**/sample.??",
+            "**/sample.???", // Matches sample.mkv
+            "**/sample.????", // Matches sample.webm
+            "**/sample.?????",
+            "**/*.sample.?",
+            "**/*.sample.??",
+            "**/*.sample.???",
+            "**/*.sample.????",
+            "**/*.sample.?????",
+            "**/sample/*",
 
             // Directories
             "**/metadata/**",
@@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
             "**/.grab/**",
             "**/.grab",
 
-            // Unix hidden files and directories
-            "**/.*/**",
+            // Unix hidden files
             "**/.*",
 
+            // Mac - if you ever remove the above.
+            // "**/._*",
+            // "**/.DS_Store",
+
             // thumbs.db
             "**/thumbs.db",
 

+ 15 - 18
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.Library
         private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
 
-        private readonly object _disposeLock = new object();
-
         private IMediaSourceProvider[] _providers;
 
         public MediaSourceManager(
@@ -623,12 +621,14 @@ namespace Emby.Server.Implementations.Library
 
             if (liveStreamInfo is IDirectStreamProvider)
             {
-                var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                {
-                    MediaSource = mediaSource,
-                    ExtractChapters = false,
-                    MediaType = DlnaProfileType.Video
-                }, cancellationToken).ConfigureAwait(false);
+                var info = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
+                    {
+                        MediaSource = mediaSource,
+                        ExtractChapters = false,
+                        MediaType = DlnaProfileType.Video
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
                 mediaSource.MediaStreams = info.MediaStreams;
                 mediaSource.Container = info.Container;
@@ -859,11 +859,11 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+        private (IMediaSourceProvider, string) GetProvider(string key)
         {
             if (string.IsNullOrEmpty(key))
             {
-                throw new ArgumentException("key");
+                throw new ArgumentException("Key can't be empty.", nameof(key));
             }
 
             var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
@@ -873,7 +873,7 @@ namespace Emby.Server.Implementations.Library
             var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
             var keyId = key.Substring(splitIndex + 1);
 
-            return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+            return (provider, keyId);
         }
 
         /// <summary>
@@ -893,15 +893,12 @@ namespace Emby.Server.Implementations.Library
         {
             if (dispose)
             {
-                lock (_disposeLock)
+                foreach (var key in _openStreams.Keys.ToList())
                 {
-                    foreach (var key in _openStreams.Keys.ToList())
-                    {
-                        var task = CloseLiveStream(key);
-
-                        Task.WaitAll(task);
-                    }
+                    CloseLiveStream(key).GetAwaiter().GetResult();
                 }
+
+                _liveStreamSemaphore.Dispose();
             }
         }
     }

+ 6 - 5
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         }
     }
 
-    public class HdHomerunManager : IDisposable
+    public sealed class HdHomerunManager : IDisposable
     {
         public const int HdHomeRunPort = 65001;
 
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     StopStreaming(socket).GetAwaiter().GetResult();
                 }
             }
+
+            GC.SuppressFinalize(this);
         }
 
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     }
 
                     _activeTuner = i;
-                    var lockKeyString = string.Format("{0:d}", lockKeyValue);
+                    var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
                     var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
                     await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
                     int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
@@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         continue;
                     }
 
-                    var commandList = commands.GetCommands();
-                    foreach (var command in commandList)
+                    foreach (var command in commands.GetCommands())
                     {
                         var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
                         await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         }
                     }
 
-                    var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
+                    var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
                     var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
 
                     await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);

+ 1 - 1
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -92,7 +92,7 @@
     "HeaderRecordingGroups": "錄製組",
     "Inherit": "繼承",
     "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
-    "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+    "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskRefreshChannels": "重新整理頻道",
     "TaskUpdatePlugins": "更新插件",

+ 23 - 20
Emby.Server.Implementations/Net/UdpSocket.cs

@@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
     public sealed class UdpSocket : ISocket, IDisposable
     {
         private Socket _socket;
-        private int _localPort;
+        private readonly int _localPort;
         private bool _disposed = false;
 
         public Socket Socket => _socket;
 
-        public IPAddress LocalIPAddress { get; }
-
         private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
         {
             SocketFlags = SocketFlags.None
@@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
             InitReceiveSocketAsyncEventArgs();
         }
 
+        public UdpSocket(Socket socket, IPEndPoint endPoint)
+        {
+            if (socket == null)
+            {
+                throw new ArgumentNullException(nameof(socket));
+            }
+
+            _socket = socket;
+            _socket.Connect(endPoint);
+
+            InitReceiveSocketAsyncEventArgs();
+        }
+
+        public IPAddress LocalIPAddress { get; }
+
         private void InitReceiveSocketAsyncEventArgs()
         {
             var receiveBuffer = new byte[8192];
             _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
-            _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+            _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
 
             var sendBuffer = new byte[8192];
             _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
-            _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
+            _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
         }
 
-        private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
         {
             var tcs = _currentReceiveTaskCompletionSource;
             if (tcs != null)
@@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
             }
         }
 
-        private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
         {
             var tcs = _currentSendTaskCompletionSource;
             if (tcs != null)
@@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
             }
         }
 
-        public UdpSocket(Socket socket, IPEndPoint endPoint)
-        {
-            if (socket == null)
-            {
-                throw new ArgumentNullException(nameof(socket));
-            }
-
-            _socket = socket;
-            _socket.Connect(endPoint);
-
-            InitReceiveSocketAsyncEventArgs();
-        }
-
         public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
         {
             ThrowIfDisposed();
@@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             if (_disposed)
@@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
             }
 
             _socket?.Dispose();
+            _receiveSocketAsyncEventArgs.Dispose();
+            _sendSocketAsyncEventArgs.Dispose();
             _currentReceiveTaskCompletionSource?.TrySetCanceled();
             _currentSendTaskCompletionSource?.TrySetCanceled();
 

+ 20 - 44
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
                         AlbumTitle = child.Album
                     };
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
 
-                    var hasArtist = child as IHasArtist;
-                    if (hasArtist != null)
+                    if (child is IHasArtist hasArtist)
                     {
-                        entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
                     }
 
                     if (child.RunTimeTicks.HasValue)
@@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
                         AlbumTitle = child.Album
                     };
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
 
-                    var hasArtist = child as IHasArtist;
-                    if (hasArtist != null)
+                    if (child is IHasArtist hasArtist)
                     {
-                        entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
                     }
 
                     if (child.RunTimeTicks.HasValue)
@@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
 
             if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
             {
-                var playlist = new M3uPlaylist();
-                playlist.IsExtended = true;
+                var playlist = new M3uPlaylist
+                {
+                    IsExtended = true
+                };
                 foreach (var child in item.GetLinkedChildren())
                 {
                     var entry = new M3uPlaylistEntry()
@@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
                         Album = child.Album
                     };
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
 
                     if (child.RunTimeTicks.HasValue)
@@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
                         Album = child.Album
                     };
 
-                    var hasAlbumArtist = child as IHasAlbumArtist;
-                    if (hasAlbumArtist != null)
+                    if (child is IHasAlbumArtist hasAlbumArtist)
                     {
-                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
                     }
 
                     if (child.RunTimeTicks.HasValue)
@@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
 
             if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
             {
-                folderPath = folderPath + Path.DirectorySeparatorChar;
+                folderPath += Path.DirectorySeparatorChar;
             }
 
             var folderUri = new Uri(folderPath);
@@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
             return relativePath;
         }
 
-        private static string UnEscape(string content)
-        {
-            if (content == null)
-            {
-                return content;
-            }
-
-            return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
-        }
-
-        private static string Escape(string content)
-        {
-            if (content == null)
-            {
-                return null;
-            }
-
-            return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
-        }
-
         public Folder GetPlaylistsFolder(Guid userId)
         {
-            var typeName = "PlaylistsFolder";
+            const string TypeName = "PlaylistsFolder";
 
-            return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
-                _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
+            return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
+                _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
         }
     }
 }

+ 14 - 4
Emby.Server.Implementations/Services/ServiceController.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.HttpServer;
 using MediaBrowser.Model.Services;
@@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
         {
             if (restPath.Path[0] != '/')
             {
-                throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Route '{0}' on '{1}' must start with a '/'",
+                        restPath.Path,
+                        restPath.RequestType.GetMethodName()));
             }
 
             if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
             {
-                throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Route '{0}' on '{1}' contains invalid chars. ",
+                        restPath.Path,
+                        restPath.RequestType.GetMethodName()));
             }
 
             if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
@@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
 
             var service = httpHost.CreateInstance(serviceType);
 
-            var serviceRequiresContext = service as IRequiresRequest;
-            if (serviceRequiresContext != null)
+            if (service is IRequiresRequest serviceRequiresContext)
             {
                 serviceRequiresContext.Request = req;
             }

+ 4 - 4
Emby.Server.Implementations/Services/ServiceHandler.cs

@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
             return null;
         }
 
-        public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
+        public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
         {
             httpReq.Items["__route"] = _restPath;
 
@@ -80,10 +80,10 @@ namespace Emby.Server.Implementations.Services
                 httpReq.ResponseContentType = _responseContentType;
             }
 
-            var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
+            var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
 
             httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-            
+
             httpRes.HttpContext.SetServiceStackRequest(httpReq);
             var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
 
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Services
             await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
         }
 
-        public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
+        public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
         {
             var requestType = restPath.RequestType;
 

+ 2 - 2
Emby.Server.Implementations/Session/SessionManager.cs

@@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
         /// </summary>
         /// <param name="info">The info.</param>
         /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">info</exception>
-        /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
+        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
         public async Task OnPlaybackStopped(PlaybackStopInfo info)
         {
             CheckDisposed();

+ 16 - 14
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
             if (session != null)
             {
                 EnsureController(session, e.Argument);
-                await KeepAliveWebSocket(e.Argument);
+                await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
             }
             else
             {
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
             // Notify WebSocket about timeout
             try
             {
-                await SendForceKeepAlive(webSocket);
+                await SendForceKeepAlive(webSocket).ConfigureAwait(false);
             }
             catch (WebSocketException exception)
             {
@@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
                 if (_keepAliveCancellationToken != null)
                 {
                     _keepAliveCancellationToken.Cancel();
+                    _keepAliveCancellationToken.Dispose();
                     _keepAliveCancellationToken = null;
                 }
             }
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
                 lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
             }
 
-            if (inactive.Any())
+            if (inactive.Count > 0)
             {
                 _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
             }
@@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
             {
                 try
                 {
-                    await SendForceKeepAlive(webSocket);
+                    await SendForceKeepAlive(webSocket).ConfigureAwait(false);
                 }
                 catch (WebSocketException exception)
                 {
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
 
             lock (_webSocketsLock)
             {
-                if (lost.Any())
+                if (lost.Count > 0)
                 {
                     _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
                     foreach (var webSocket in lost)
@@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
                     }
                 }
 
-                if (!_webSockets.Any())
+                if (_webSockets.Count == 0)
                 {
                     StopKeepAlive();
                 }
@@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
         /// <returns>Task.</returns>
         private Task SendForceKeepAlive(IWebSocketConnection webSocket)
         {
-            return webSocket.SendAsync(new WebSocketMessage<int>
-            {
-                MessageType = "ForceKeepAlive",
-                Data = WebSocketLostTimeout
-            }, CancellationToken.None);
+            return webSocket.SendAsync(
+                new WebSocketMessage<int>
+                {
+                    MessageType = "ForceKeepAlive",
+                    Data = WebSocketLostTimeout
+                },
+                CancellationToken.None);
         }
 
         /// <summary>
@@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
         {
             while (!cancellationToken.IsCancellationRequested)
             {
-                await callback();
-                Task task = Task.Delay(interval, cancellationToken);
+                await callback().ConfigureAwait(false);
 
                 try
                 {
-                    await task;
+                    await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
                 }
                 catch (TaskCanceledException)
                 {

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

@@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
 
         private static int CompareEpisodes(Episode x, Episode y)
         {
-            var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
-            var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
+            var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
+            var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
 
             return xValue.CompareTo(yValue);
         }

+ 32 - 28
Emby.Server.Implementations/SyncPlay/SyncPlayController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
             /// 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>
@@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
         /// </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>
@@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
             _syncPlayManager = syncPlayManager;
         }
 
+        /// <inheritdoc />
+        public Guid GetGroupId() => _group.GroupId;
+
+        /// <inheritdoc />
+        public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+        /// <inheritdoc />
+        public bool IsGroupEmpty() => _group.IsEmpty();
+
         /// <summary>
         /// Converts DateTime to UTC string.
         /// </summary>
@@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
         /// <value>The UTC string.</value>
         private string DateToUTCString(DateTime date)
         {
-            return date.ToUniversalTime().ToString("o");
+            return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
         }
 
         /// <summary>
@@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
         /// <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)
+        private IEnumerable<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();
+                    return _group.Participants.Values
+                        .Select(session => session.Session);
                 case BroadcastType.AllExceptCurrentSession:
-                    return _group.Participants.Values.Select(
-                        session => session.Session).Where(
-                        session => !session.Id.Equals(from.Id)).ToArray();
+                    return _group.Participants.Values
+                        .Select(session => session.Session)
+                        .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
                 case BroadcastType.AllReady:
-                    return _group.Participants.Values.Where(
-                        session => !session.IsBuffering).Select(
-                        session => session.Session).ToArray();
+                    return _group.Participants.Values
+                        .Where(session => !session.IsBuffering)
+                        .Select(session => session.Session);
                 default:
                     return Array.Empty<SessionInfo>();
             }
@@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             IEnumerable<Task> GetTasks()
             {
-                SessionInfo[] sessions = FilterSessions(from, type);
-                foreach (var session in sessions)
+                foreach (var session in FilterSessions(from, type))
                 {
-                    yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
+                    yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
                 }
             }
 
@@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             IEnumerable<Task> GetTasks()
             {
-                SessionInfo[] sessions = FilterSessions(from, type);
-                foreach (var session in sessions)
+                foreach (var session in FilterSessions(from, type))
                 {
-                    yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
+                    yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
                 }
             }
 
@@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
             }
             else
             {
-                var playRequest = new PlayRequest();
-                playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
-                playRequest.StartPositionTicks = _group.PositionTicks;
+                var playRequest = new PlayRequest
+                {
+                    ItemIds = new Guid[] { _group.PlayingItem.Id },
+                    StartPositionTicks = _group.PositionTicks
+                };
                 var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
                 SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
             }

+ 9 - 13
Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs

@@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia
         /// <param name="percent">The percentage played to display with the indicator.</param>
         public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
         {
-            using (var paint = new SKPaint())
-            {
-                var endX = imageSize.Width - 1;
-                var endY = imageSize.Height - 1;
+            using var paint = new SKPaint();
+            var endX = imageSize.Width - 1;
+            var endY = imageSize.Height - 1;
 
-                paint.Color = SKColor.Parse("#99000000");
-                paint.Style = SKPaintStyle.Fill;
-                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
+            paint.Color = SKColor.Parse("#99000000");
+            paint.Style = SKPaintStyle.Fill;
+            canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
 
-                double foregroundWidth = endX;
-                foregroundWidth *= percent;
-                foregroundWidth /= 100;
+            double foregroundWidth = (endX * percent) / 100;
 
-                paint.Color = SKColor.Parse("#FF00A4DC");
-                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
-            }
+            paint.Color = SKColor.Parse("#FF00A4DC");
+            canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
         }
     }
 }

+ 15 - 19
Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia
         {
             var x = imageSize.Width - OffsetFromTopRightCorner;
 
-            using (var paint = new SKPaint())
+            using var paint = new SKPaint
             {
-                paint.Color = SKColor.Parse("#CC00A4DC");
-                paint.Style = SKPaintStyle.Fill;
-                canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
-            }
+                Color = SKColor.Parse("#CC00A4DC"),
+                Style = SKPaintStyle.Fill
+            };
 
-            using (var paint = new SKPaint())
-            {
-                paint.Color = new SKColor(255, 255, 255, 255);
-                paint.Style = SKPaintStyle.Fill;
+            canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
 
-                paint.TextSize = 30;
-                paint.IsAntialias = true;
+            paint.Color = new SKColor(255, 255, 255, 255);
+            paint.TextSize = 30;
+            paint.IsAntialias = true;
 
-                // or:
-                // var emojiChar = 0x1F680;
-                const string Text = "✔️";
-                var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
+            // or:
+            // var emojiChar = 0x1F680;
+            const string Text = "✔️";
+            var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
 
-                // ask the font manager for a font with that character
-                paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
+            // ask the font manager for a font with that character
+            paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
 
-                canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
-            }
+            canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
         }
     }
 }

+ 1 - 1
Jellyfin.Drawing.Skia/SkiaCodecException.cs

@@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia
         /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
         /// </summary>
         /// <param name="result">The non-successful codec result returned by Skia.</param>
-        public SkiaCodecException(SKCodecResult result) : base()
+        public SkiaCodecException(SKCodecResult result)
         {
             CodecResult = result;
         }

+ 162 - 310
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
         /// </summary>
         /// <param name="logger">The application logger.</param>
         /// <param name="appPaths">The application paths.</param>
-        public SkiaEncoder(
-            ILogger<SkiaEncoder> logger,
-            IApplicationPaths appPaths)
+        public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
         {
             _logger = logger;
             _appPaths = appPaths;
@@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
         /// <returns>The converted format.</returns>
         public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
         {
-            switch (selectedFormat)
-            {
-                case ImageFormat.Bmp:
-                    return SKEncodedImageFormat.Bmp;
-                case ImageFormat.Jpg:
-                    return SKEncodedImageFormat.Jpeg;
-                case ImageFormat.Gif:
-                    return SKEncodedImageFormat.Gif;
-                case ImageFormat.Webp:
-                    return SKEncodedImageFormat.Webp;
-                default:
-                    return SKEncodedImageFormat.Png;
-            }
+            return selectedFormat switch
+            {
+                ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
+                ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
+                ImageFormat.Gif => SKEncodedImageFormat.Gif,
+                ImageFormat.Webp => SKEncodedImageFormat.Webp,
+                _ => SKEncodedImageFormat.Png
+            };
         }
 
         private static bool IsTransparentRow(SKBitmap bmp, int row)
@@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
         private SKBitmap CropWhiteSpace(SKBitmap bitmap)
         {
             var topmost = 0;
-            for (int row = 0; row < bitmap.Height; ++row)
+            while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
             {
-                if (IsTransparentRow(bitmap, row))
-                {
-                    topmost = row + 1;
-                }
-                else
-                {
-                    break;
-                }
+                topmost++;
             }
 
             int bottommost = bitmap.Height;
-            for (int row = bitmap.Height - 1; row >= 0; --row)
+            while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1))
             {
-                if (IsTransparentRow(bitmap, row))
-                {
-                    bottommost = row;
-                }
-                else
-                {
-                    break;
-                }
+                bottommost--;
             }
 
-            int leftmost = 0, rightmost = bitmap.Width;
-            for (int col = 0; col < bitmap.Width; ++col)
+            var leftmost = 0;
+            while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
             {
-                if (IsTransparentColumn(bitmap, col))
-                {
-                    leftmost = col + 1;
-                }
-                else
-                {
-                    break;
-                }
+                leftmost++;
             }
 
-            for (int col = bitmap.Width - 1; col >= 0; --col)
+            var rightmost = bitmap.Width;
+            while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1))
             {
-                if (IsTransparentColumn(bitmap, col))
-                {
-                    rightmost = col;
-                }
-                else
-                {
-                    break;
-                }
+                rightmost--;
             }
 
             var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
 
-            using (var image = SKImage.FromBitmap(bitmap))
-            using (var subset = image.Subset(newRect))
-            {
-                return SKBitmap.FromImage(subset);
-            }
+            using var image = SKImage.FromBitmap(bitmap);
+            using var subset = image.Subset(newRect);
+            return SKBitmap.FromImage(subset);
         }
 
         /// <inheritdoc />
@@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
                 throw new FileNotFoundException("File not found", path);
             }
 
-            using (var codec = SKCodec.Create(path, out SKCodecResult result))
-            {
-                EnsureSuccess(result);
+            using var codec = SKCodec.Create(path, out SKCodecResult result);
+            EnsureSuccess(result);
 
-                var info = codec.Info;
+            var info = codec.Info;
 
-                return new ImageDimensions(info.Width, info.Height);
-            }
+            return new ImageDimensions(info.Width, info.Height);
         }
 
         /// <inheritdoc />
@@ -253,12 +215,7 @@ namespace Jellyfin.Drawing.Skia
                 }
             }
 
-            if (HasDiacritics(path))
-            {
-                return true;
-            }
-
-            return false;
+            return HasDiacritics(path);
         }
 
         private string NormalizePath(string path)
@@ -283,25 +240,17 @@ namespace Jellyfin.Drawing.Skia
                 return SKEncodedOrigin.TopLeft;
             }
 
-            switch (orientation.Value)
-            {
-                case ImageOrientation.TopRight:
-                    return SKEncodedOrigin.TopRight;
-                case ImageOrientation.RightTop:
-                    return SKEncodedOrigin.RightTop;
-                case ImageOrientation.RightBottom:
-                    return SKEncodedOrigin.RightBottom;
-                case ImageOrientation.LeftTop:
-                    return SKEncodedOrigin.LeftTop;
-                case ImageOrientation.LeftBottom:
-                    return SKEncodedOrigin.LeftBottom;
-                case ImageOrientation.BottomRight:
-                    return SKEncodedOrigin.BottomRight;
-                case ImageOrientation.BottomLeft:
-                    return SKEncodedOrigin.BottomLeft;
-                default:
-                    return SKEncodedOrigin.TopLeft;
-            }
+            return orientation.Value switch
+            {
+                ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
+                ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
+                ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
+                ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
+                ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
+                ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
+                ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
+                _ => SKEncodedOrigin.TopLeft
+            };
         }
 
         /// <summary>
@@ -323,24 +272,22 @@ namespace Jellyfin.Drawing.Skia
 
             if (requiresTransparencyHack || forceCleanBitmap)
             {
-                using (var codec = SKCodec.Create(NormalizePath(path)))
+                using var codec = SKCodec.Create(NormalizePath(path));
+                if (codec == null)
                 {
-                    if (codec == null)
-                    {
-                        origin = GetSKEncodedOrigin(orientation);
-                        return null;
-                    }
+                    origin = GetSKEncodedOrigin(orientation);
+                    return null;
+                }
 
-                    // create the bitmap
-                    var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
+                // create the bitmap
+                var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 
-                    // decode
-                    _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+                // decode
+                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 
-                    origin = codec.EncodedOrigin;
+                origin = codec.EncodedOrigin;
 
-                    return bitmap;
-                }
+                return bitmap;
             }
 
             var resultBitmap = SKBitmap.Decode(NormalizePath(path));
@@ -367,15 +314,8 @@ namespace Jellyfin.Drawing.Skia
         {
             if (cropWhitespace)
             {
-                using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
-                {
-                    if (bitmap == null)
-                    {
-                        return null;
-                    }
-
-                    return CropWhiteSpace(bitmap);
-                }
+                using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
+                return bitmap == null ? null : CropWhiteSpace(bitmap);
             }
 
             return Decode(path, forceAnalyzeBitmap, orientation, out origin);
@@ -403,133 +343,55 @@ namespace Jellyfin.Drawing.Skia
 
         private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
         {
+            if (origin == SKEncodedOrigin.Default)
+            {
+                return bitmap;
+            }
+
+            var needsFlip = origin == SKEncodedOrigin.LeftBottom
+                            || origin == SKEncodedOrigin.LeftTop
+                            || origin == SKEncodedOrigin.RightBottom
+                            || origin == SKEncodedOrigin.RightTop;
+            var rotated = needsFlip
+                ? new SKBitmap(bitmap.Height, bitmap.Width)
+                : new SKBitmap(bitmap.Width, bitmap.Height);
+            using var surface = new SKCanvas(rotated);
+            var midX = (float)rotated.Width / 2;
+            var midY = (float)rotated.Height / 2;
+
             switch (origin)
             {
                 case SKEncodedOrigin.TopRight:
-                    {
-                        var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
-                        using (var surface = new SKCanvas(rotated))
-                        {
-                            surface.Translate(rotated.Width, 0);
-                            surface.Scale(-1, 1);
-                            surface.DrawBitmap(bitmap, 0, 0);
-                        }
-
-                        return rotated;
-                    }
-
+                    surface.Scale(-1, 1, midX, midY);
+                    break;
                 case SKEncodedOrigin.BottomRight:
-                    {
-                        var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
-                        using (var surface = new SKCanvas(rotated))
-                        {
-                            float px = (float)bitmap.Width / 2;
-                            float py = (float)bitmap.Height / 2;
-
-                            surface.RotateDegrees(180, px, py);
-                            surface.DrawBitmap(bitmap, 0, 0);
-                        }
-
-                        return rotated;
-                    }
-
+                    surface.RotateDegrees(180, midX, midY);
+                    break;
                 case SKEncodedOrigin.BottomLeft:
-                    {
-                        var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
-                        using (var surface = new SKCanvas(rotated))
-                        {
-                            float px = (float)bitmap.Width / 2;
-
-                            float py = (float)bitmap.Height / 2;
-
-                            surface.Translate(rotated.Width, 0);
-                            surface.Scale(-1, 1);
-
-                            surface.RotateDegrees(180, px, py);
-                            surface.DrawBitmap(bitmap, 0, 0);
-                        }
-
-                        return rotated;
-                    }
-
+                    surface.Scale(1, -1, midX, midY);
+                    break;
                 case SKEncodedOrigin.LeftTop:
-                    {
-                        // TODO: Remove dual canvases, had trouble with flipping
-                        using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
-                        {
-                            using (var surface = new SKCanvas(rotated))
-                            {
-                                surface.Translate(rotated.Width, 0);
-
-                                surface.RotateDegrees(90);
-
-                                surface.DrawBitmap(bitmap, 0, 0);
-                            }
-
-                            var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
-                            using (var flippedCanvas = new SKCanvas(flippedBitmap))
-                            {
-                                flippedCanvas.Translate(flippedBitmap.Width, 0);
-                                flippedCanvas.Scale(-1, 1);
-                                flippedCanvas.DrawBitmap(rotated, 0, 0);
-                            }
-
-                            return flippedBitmap;
-                        }
-                    }
-
+                    surface.Translate(0, -rotated.Height);
+                    surface.Scale(1, -1, midX, midY);
+                    surface.RotateDegrees(-90);
+                    break;
                 case SKEncodedOrigin.RightTop:
-                    {
-                        var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
-                        using (var surface = new SKCanvas(rotated))
-                        {
-                            surface.Translate(rotated.Width, 0);
-                            surface.RotateDegrees(90);
-                            surface.DrawBitmap(bitmap, 0, 0);
-                        }
-
-                        return rotated;
-                    }
-
+                    surface.Translate(rotated.Width, 0);
+                    surface.RotateDegrees(90);
+                    break;
                 case SKEncodedOrigin.RightBottom:
-                    {
-                        // TODO: Remove dual canvases, had trouble with flipping
-                        using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
-                        {
-                            using (var surface = new SKCanvas(rotated))
-                            {
-                                surface.Translate(0, rotated.Height);
-                                surface.RotateDegrees(270);
-                                surface.DrawBitmap(bitmap, 0, 0);
-                            }
-
-                            var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
-                            using (var flippedCanvas = new SKCanvas(flippedBitmap))
-                            {
-                                flippedCanvas.Translate(flippedBitmap.Width, 0);
-                                flippedCanvas.Scale(-1, 1);
-                                flippedCanvas.DrawBitmap(rotated, 0, 0);
-                            }
-
-                            return flippedBitmap;
-                        }
-                    }
-
+                    surface.Translate(rotated.Width, 0);
+                    surface.Scale(1, -1, midX, midY);
+                    surface.RotateDegrees(90);
+                    break;
                 case SKEncodedOrigin.LeftBottom:
-                    {
-                        var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
-                        using (var surface = new SKCanvas(rotated))
-                        {
-                            surface.Translate(0, rotated.Height);
-                            surface.RotateDegrees(270);
-                            surface.DrawBitmap(bitmap, 0, 0);
-                        }
-
-                        return rotated;
-                    }
-
-                default: return bitmap;
+                    surface.Translate(0, rotated.Height);
+                    surface.RotateDegrees(-90);
+                    break;
             }
+
+            surface.DrawBitmap(bitmap, 0, 0);
+            return rotated;
         }
 
         /// <inheritdoc/>
@@ -552,97 +414,87 @@ namespace Jellyfin.Drawing.Skia
             var blur = options.Blur ?? 0;
             var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 
-            using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation))
+            using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation);
+            if (bitmap == null)
             {
-                if (bitmap == null)
-                {
-                    throw new InvalidDataException($"Skia unable to read image {inputPath}");
-                }
+                throw new InvalidDataException($"Skia unable to read image {inputPath}");
+            }
 
-                var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
+            var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 
-                if (!options.CropWhiteSpace
-                    && options.HasDefaultOptions(inputPath, originalImageSize)
-                    && !autoOrient)
-                {
-                    // Just spit out the original file if all the options are default
-                    return inputPath;
-                }
+            if (!options.CropWhiteSpace
+                && options.HasDefaultOptions(inputPath, originalImageSize)
+                && !autoOrient)
+            {
+                // Just spit out the original file if all the options are default
+                return inputPath;
+            }
 
-                var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+            var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 
-                var width = newImageSize.Width;
-                var height = newImageSize.Height;
+            var width = newImageSize.Width;
+            var height = newImageSize.Height;
+
+            using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType);
+            // scale image
+            bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+
+            // If all we're doing is resizing then we can stop now
+            if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+            {
+                Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+                using var outputStream = new SKFileWStream(outputPath);
+                using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
+                pixmap.Encode(outputStream, skiaOutputFormat, quality);
+                return outputPath;
+            }
 
-                using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+            // create bitmap to use for canvas drawing used to draw into bitmap
+            using var saveBitmap = new SKBitmap(width, height);
+            using var canvas = new SKCanvas(saveBitmap);
+            // set background color if present
+            if (hasBackgroundColor)
+            {
+                canvas.Clear(SKColor.Parse(options.BackgroundColor));
+            }
+
+            // Add blur if option is present
+            if (blur > 0)
+            {
+                // create image from resized bitmap to apply blur
+                using var paint = new SKPaint();
+                using var filter = SKImageFilter.CreateBlur(blur, blur);
+                paint.ImageFilter = filter;
+                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+            }
+            else
+            {
+                // draw resized bitmap onto canvas
+                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+            }
+
+            // If foreground layer present then draw
+            if (hasForegroundColor)
+            {
+                if (!double.TryParse(options.ForegroundLayer, out double opacity))
                 {
-                    // scale image
-                    bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+                    opacity = .4;
+                }
 
-                    // If all we're doing is resizing then we can stop now
-                    if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
-                    {
-                        Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
-                        using (var outputStream = new SKFileWStream(outputPath))
-                        using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
-                        {
-                            pixmap.Encode(outputStream, skiaOutputFormat, quality);
-                            return outputPath;
-                        }
-                    }
+                canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+            }
 
-                    // create bitmap to use for canvas drawing used to draw into bitmap
-                    using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType))
-                    using (var canvas = new SKCanvas(saveBitmap))
-                    {
-                        // set background color if present
-                        if (hasBackgroundColor)
-                        {
-                            canvas.Clear(SKColor.Parse(options.BackgroundColor));
-                        }
-
-                        // Add blur if option is present
-                        if (blur > 0)
-                        {
-                            // create image from resized bitmap to apply blur
-                            using (var paint = new SKPaint())
-                            using (var filter = SKImageFilter.CreateBlur(blur, blur))
-                            {
-                                paint.ImageFilter = filter;
-                                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
-                            }
-                        }
-                        else
-                        {
-                            // draw resized bitmap onto canvas
-                            canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
-                        }
-
-                        // If foreground layer present then draw
-                        if (hasForegroundColor)
-                        {
-                            if (!double.TryParse(options.ForegroundLayer, out double opacity))
-                            {
-                                opacity = .4;
-                            }
-
-                            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
-                        }
-
-                        if (hasIndicator)
-                        {
-                            DrawIndicator(canvas, width, height, options);
-                        }
-
-                        Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
-                        using (var outputStream = new SKFileWStream(outputPath))
-                        {
-                            using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
-                            {
-                                pixmap.Encode(outputStream, skiaOutputFormat, quality);
-                            }
-                        }
-                    }
+            if (hasIndicator)
+            {
+                DrawIndicator(canvas, width, height, options);
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            using (var outputStream = new SKFileWStream(outputPath))
+            {
+                using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
+                {
+                    pixmap.Encode(outputStream, skiaOutputFormat, quality);
                 }
             }
 

+ 1 - 1
Jellyfin.Drawing.Skia/SkiaException.cs

@@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia
         /// <summary>
         /// Initializes a new instance of the <see cref="SkiaException"/> class.
         /// </summary>
-        public SkiaException() : base()
+        public SkiaException()
         {
         }
 

+ 51 - 69
Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia
         /// <param name="height">The desired height of the collage.</param>
         public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
         {
-            using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
-            using (var outputStream = new SKFileWStream(outputPath))
-            using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
-            {
-                pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
-            }
+            using var bitmap = BuildSquareCollageBitmap(paths, width, height);
+            using var outputStream = new SKFileWStream(outputPath);
+            using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+            pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
         }
 
         /// <summary>
@@ -86,56 +84,46 @@ namespace Jellyfin.Drawing.Skia
         /// <param name="height">The desired height of the collage.</param>
         public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
         {
-            using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
-            using (var outputStream = new SKFileWStream(outputPath))
-            using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
-            {
-                pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
-            }
+            using var bitmap = BuildThumbCollageBitmap(paths, width, height);
+            using var outputStream = new SKFileWStream(outputPath);
+            using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+            pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
         }
 
         private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
         {
             var bitmap = new SKBitmap(width, height);
 
-            using (var canvas = new SKCanvas(bitmap))
-            {
-                canvas.Clear(SKColors.Black);
+            using var canvas = new SKCanvas(bitmap);
+            canvas.Clear(SKColors.Black);
 
-                // number of images used in the thumbnail
-                var iCount = 3;
+            // number of images used in the thumbnail
+            var iCount = 3;
 
-                // determine sizes for each image that will composited into the final image
-                var iSlice = Convert.ToInt32(width / iCount);
-                int iHeight = Convert.ToInt32(height * 1.00);
-                int imageIndex = 0;
-                for (int i = 0; i < iCount; i++)
+            // determine sizes for each image that will composited into the final image
+            var iSlice = Convert.ToInt32(width / iCount);
+            int iHeight = Convert.ToInt32(height * 1.00);
+            int imageIndex = 0;
+            for (int i = 0; i < iCount; i++)
+            {
+                using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
+                imageIndex = newIndex;
+                if (currentBitmap == null)
                 {
-                    using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
-                    {
-                        imageIndex = newIndex;
-                        if (currentBitmap == null)
-                        {
-                            continue;
-                        }
-
-                        // resize to the same aspect as the original
-                        int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
-                        using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
-                        {
-                            currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
-
-                            // crop image
-                            int ix = Math.Abs((iWidth - iSlice) / 2);
-                            using (var image = SKImage.FromBitmap(resizeBitmap))
-                            using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
-                            {
-                                // draw image onto canvas
-                                canvas.DrawImage(subset ?? image, iSlice * i, 0);
-                            }
-                        }
-                    }
+                    continue;
                 }
+
+                // resize to the same aspect as the original
+                int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
+                using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
+                currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
+
+                // crop image
+                int ix = Math.Abs((iWidth - iSlice) / 2);
+                using var image = SKImage.FromBitmap(resizeBitmap);
+                using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
+                // draw image onto canvas
+                canvas.DrawImage(subset ?? image, iSlice * i, 0);
             }
 
             return bitmap;
@@ -176,33 +164,27 @@ namespace Jellyfin.Drawing.Skia
             var cellWidth = width / 2;
             var cellHeight = height / 2;
 
-            using (var canvas = new SKCanvas(bitmap))
+            using var canvas = new SKCanvas(bitmap);
+            for (var x = 0; x < 2; x++)
             {
-                for (var x = 0; x < 2; x++)
+                for (var y = 0; y < 2; y++)
                 {
-                    for (var y = 0; y < 2; y++)
+                    using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
+                    imageIndex = newIndex;
+
+                    if (currentBitmap == null)
                     {
-                        using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
-                        {
-                            imageIndex = newIndex;
-
-                            if (currentBitmap == null)
-                            {
-                                continue;
-                            }
-
-                            using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
-                            {
-                                // scale image
-                                currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
-                                // draw this image into the strip at the next position
-                                var xPos = x * cellWidth;
-                                var yPos = y * cellHeight;
-                                canvas.DrawBitmap(resizedBitmap, xPos, yPos);
-                            }
-                        }
+                        continue;
                     }
+
+                    using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
+                    // scale image
+                    currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+
+                    // draw this image into the strip at the next position
+                    var xPos = x * cellWidth;
+                    var yPos = y * cellHeight;
+                    canvas.DrawBitmap(resizedBitmap, xPos, yPos);
                 }
             }
 

+ 24 - 28
Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs

@@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia
             var x = imageSize.Width - OffsetFromTopRightCorner;
             var text = count.ToString(CultureInfo.InvariantCulture);
 
-            using (var paint = new SKPaint())
+            using var paint = new SKPaint
             {
-                paint.Color = SKColor.Parse("#CC00A4DC");
-                paint.Style = SKPaintStyle.Fill;
-                canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
-            }
-
-            using (var paint = new SKPaint())
-            {
-                paint.Color = new SKColor(255, 255, 255, 255);
-                paint.Style = SKPaintStyle.Fill;
+                Color = SKColor.Parse("#CC00A4DC"),
+                Style = SKPaintStyle.Fill
+            };
 
-                paint.TextSize = 24;
-                paint.IsAntialias = true;
+            canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
 
-                var y = OffsetFromTopRightCorner + 9;
+            paint.Color = new SKColor(255, 255, 255, 255);
+            paint.TextSize = 24;
+            paint.IsAntialias = true;
 
-                if (text.Length == 1)
-                {
-                    x -= 7;
-                }
+            var y = OffsetFromTopRightCorner + 9;
 
-                if (text.Length == 2)
-                {
-                    x -= 13;
-                }
-                else if (text.Length >= 3)
-                {
-                    x -= 15;
-                    y -= 2;
-                    paint.TextSize = 18;
-                }
+            if (text.Length == 1)
+            {
+                x -= 7;
+            }
 
-                canvas.DrawText(text, x, y, paint);
+            if (text.Length == 2)
+            {
+                x -= 13;
+            }
+            else if (text.Length >= 3)
+            {
+                x -= 15;
+                y -= 2;
+                paint.TextSize = 18;
             }
+
+            canvas.DrawText(text, x, y, paint);
         }
     }
 }

+ 2 - 2
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -766,8 +766,8 @@ namespace Jellyfin.Server.Implementations.Users
         {
             // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
             // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
-            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
-            return Regex.IsMatch(name, @"^[\w\-'._@]*$");
+            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
+            return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
         }
 
         private IAuthenticationProvider GetAuthenticationProvider(User user)

+ 6 - 0
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -457,6 +457,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+            var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
 
             if (!IsCopyCodec(outputVideoCodec))
             {
@@ -529,6 +530,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                             .Append(' ')
                             .Append("-filter_hw_device ocl ");
                     }
+
+                if (state.IsVideoRequest
+                    && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+                {
+                    arg.Append("-hwaccel videotoolbox ");
                 }
             }
 

+ 7 - 1
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -174,7 +174,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 inputFiles = new[] { mediaSource.Path };
             }
 
-            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
+            var protocol = mediaSource.Protocol;
+            if (subtitleStream.IsExternal)
+            {
+                protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
+            }
+
+            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
 
             var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
 

+ 3 - 0
MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs

@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
         public override string Description => "Get artist and album metadata or images from AudioDB.";
 
+        // TODO remove when plugin removed from server.
+        public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
+
         public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
             : base(applicationPaths, xmlSerializer)
         {

+ 3 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs

@@ -23,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
 
         public const long DefaultRateLimit = 2000u;
 
+        // TODO remove when plugin removed from server.
+        public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
+
         public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
             : base(applicationPaths, xmlSerializer)
         {

+ 3 - 0
MediaBrowser.Providers/Plugins/Omdb/Plugin.cs

@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
         public override string Description => "Get metadata for movies and other video content from OMDb.";
 
+        // TODO remove when plugin removed from server.
+        public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
+
         public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
             : base(applicationPaths, xmlSerializer)
         {

+ 3 - 0
MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs

@@ -17,6 +17,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public override string Description => "Get metadata for movies and other video content from TheTVDB.";
 
+        // TODO remove when plugin removed from server.
+        public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
+
         public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
             : base(applicationPaths, xmlSerializer)
         {

+ 13 - 1
tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

@@ -9,17 +9,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
         [InlineData("/media/small.jpg", true)]
         [InlineData("/media/albumart.jpg", true)]
         [InlineData("/media/movie.sample.mp4", true)]
+        [InlineData("/media/movie/sample.mp4", true)]
+        [InlineData("/media/movie/sample/movie.mp4", true)]
+        [InlineData("/foo/sample/bar/baz.mkv", false)]
+        [InlineData("/media/movies/the sample/the sample.mkv", false)]
+        [InlineData("/media/movies/sampler.mkv", false)]
         [InlineData("/media/movies/#Recycle/test.txt", true)]
         [InlineData("/media/movies/#recycle/", true)]
         [InlineData("/media/movies/#recycle", true)]
         [InlineData("thumbs.db", true)]
         [InlineData(@"C:\media\movies\movie.avi", false)]
-        [InlineData("/media/.hiddendir/file.mp4", true)]
+        [InlineData("/media/.hiddendir/file.mp4", false)]
         [InlineData("/media/dir/.hiddenfile.mp4", true)]
+        [InlineData("/media/dir/._macjunk.mp4", true)]
         [InlineData("/volume1/video/Series/@eaDir", true)]
         [InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
         [InlineData("/directory/@Recycle", true)]
         [InlineData("/directory/@Recycle/file.mp3", true)]
+        [InlineData("/media/movies/.@__thumb", true)]
+        [InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)]
+        [InlineData("/media/music/Foo B.A.R./epic.flac", false)]
+        [InlineData("/media/music/Foo B.A.R", false)]
+        // This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78
+        // [InlineData("/media/music/Foo B.A.R.", false)]
         public void PathIgnored(string path, bool expected)
         {
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));