2
0
cvium 4 жил өмнө
parent
commit
1b49435a0e

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

@@ -1,7 +1,7 @@
 using System;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Naming.Audio
 {
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
         /// <returns>True if file at path is audio file.</returns>
         public static bool IsAudioFile(string path, NamingOptions options)
         {
-            var extension = Path.GetExtension(path);
-            return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
     }
 }

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

@@ -27,6 +27,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
   </ItemGroup>
 

+ 49 - 46
Emby.Naming/Video/ExtraResolver.cs

@@ -29,70 +29,73 @@ namespace Emby.Naming.Video
         /// <param name="path">Path to file.</param>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
         public ExtraResult GetExtraInfo(string path)
-        {
-            return _options.VideoExtraRules
-                .Select(i => GetExtraInfo(path, i))
-                .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
-        }
-
-        private ExtraResult GetExtraInfo(string path, ExtraRule rule)
         {
             var result = new ExtraResult();
 
-            if (rule.MediaType == MediaType.Audio)
+            for (var i = 0; i < _options.VideoExtraRules.Length; i++)
             {
-                if (!AudioFileParser.IsAudioFile(path, _options))
+                var rule = _options.VideoExtraRules[i];
+                if (rule.MediaType == MediaType.Audio)
                 {
-                    return result;
+                    if (!AudioFileParser.IsAudioFile(path, _options))
+                    {
+                        continue;
+                    }
                 }
-            }
-            else if (rule.MediaType == MediaType.Video)
-            {
-                if (!new VideoResolver(_options).IsVideoFile(path))
+                else if (rule.MediaType == MediaType.Video)
                 {
-                    return result;
+                    if (!new VideoResolver(_options).IsVideoFile(path))
+                    {
+                        continue;
+                    }
                 }
-            }
-
-            if (rule.RuleType == ExtraRuleType.Filename)
-            {
-                var filename = Path.GetFileNameWithoutExtension(path);
 
-                if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
+                var pathSpan = path.AsSpan();
+                if (rule.RuleType == ExtraRuleType.Filename)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
-                }
-            }
-            else if (rule.RuleType == ExtraRuleType.Suffix)
-            {
-                var filename = Path.GetFileNameWithoutExtension(path);
+                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
 
-                if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
+                    if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
+                }
+                else if (rule.RuleType == ExtraRuleType.Suffix)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
+
+                    if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
                 }
-            }
-            else if (rule.RuleType == ExtraRuleType.Regex)
-            {
-                var filename = Path.GetFileName(path);
+                else if (rule.RuleType == ExtraRuleType.Regex)
+                {
+                    var filename = Path.GetFileName(path);
 
-                var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+                    var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
 
-                if (regex.IsMatch(filename))
+                    if (regex.IsMatch(filename))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
+                }
+                else if (rule.RuleType == ExtraRuleType.DirectoryName)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+                    if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
                 }
-            }
-            else if (rule.RuleType == ExtraRuleType.DirectoryName)
-            {
-                var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
-                if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
+
+                if (result.ExtraType != null)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    return result;
                 }
             }
 

+ 10 - 12
Emby.Naming/Video/VideoResolver.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Naming.Video
 {
@@ -59,15 +59,15 @@ namespace Emby.Naming.Video
             }
 
             bool isStub = false;
-            string? container = null;
+            ReadOnlySpan<char> container = null;
             string? stubType = null;
 
             if (!isDirectory)
             {
-                var extension = Path.GetExtension(path);
+                var extension = Path.GetExtension(path.AsSpan());
 
                 // Check supported extensions
-                if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     // It's not supported. Check stub extensions
                     if (!StubResolver.TryResolveFile(path, _options, out stubType))
@@ -86,9 +86,7 @@ namespace Emby.Naming.Video
 
             var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
 
-            var name = isDirectory
-                ? Path.GetFileName(path)
-                : Path.GetFileNameWithoutExtension(path);
+            var name = Path.GetFileNameWithoutExtension(path);
 
             int? year = null;
 
@@ -107,7 +105,7 @@ namespace Emby.Naming.Video
 
             return new VideoFileInfo(
                 path: path,
-                container: container,
+                container: container.ToString(),
                 isStub: isStub,
                 name: name,
                 year: year,
@@ -126,8 +124,8 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file.</returns>
         public bool IsVideoFile(string path)
         {
-            var extension = Path.GetExtension(path);
-            return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>
@@ -137,8 +135,8 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file stub.</returns>
         public bool IsStubFile(string path)
         {
-            var extension = Path.GetExtension(path);
-            return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>

+ 2 - 4
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -181,11 +181,9 @@ namespace Emby.Server.Implementations.Data
 
             foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
             {
-                if (row[1].SQLiteType != SQLiteType.Null)
+                if (row.TryGetString(1, out var columnName))
                 {
-                    var name = row[1].ToString();
-
-                    columnNames.Add(name);
+                    columnNames.Add(columnName);
                 }
             }
 

+ 100 - 10
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using SQLitePCL.pretty;
 
@@ -96,21 +97,42 @@ namespace Emby.Server.Implementations.Data
                 DateTimeStyles.None).ToUniversalTime();
         }
 
-        public static DateTime? TryReadDateTime(this IResultSetValue result)
+        public static bool TryReadDateTime(this IReadOnlyList<IResultSetValue> reader, int index, [NotNullWhen(true)] out DateTime? result)
         {
-            var dateText = result.ToString();
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            var dateText = item.ToString();
 
             if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
             {
-                return dateTimeResult.ToUniversalTime();
+                result = dateTimeResult.ToUniversalTime();
+                return true;
+            }
+
+            return false;
+        }
+
+        public static bool TryGetGuid(this IReadOnlyList<IResultSetValue> reader, int index, [NotNullWhen(true)] out Guid? result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
             }
 
-            return null;
+            result = item.ReadGuidFromBlob();
+            return true;
         }
 
-        public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
+        private static bool IsDbNull(this IResultSetValue result)
         {
-            return result[index].SQLiteType == SQLiteType.Null;
+            return result.SQLiteType == SQLiteType.Null;
         }
 
         public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
@@ -118,14 +140,48 @@ namespace Emby.Server.Implementations.Data
             return result[index].ToString();
         }
 
+        public static bool TryGetString(this IReadOnlyList<IResultSetValue> reader, int index, out string result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToString();
+            return true;
+        }
+
         public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
         {
             return result[index].ToBool();
         }
 
-        public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool TryGetBoolean(this IReadOnlyList<IResultSetValue> reader, int index, [NotNullWhen(true)] out bool? result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToBool();
+            return true;
+        }
+
+        public static bool TryGetInt(this IReadOnlyList<IResultSetValue> reader, int index, out int? result)
         {
-            return result[index].ToInt();
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToInt();
+            return true;
         }
 
         public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
@@ -133,9 +189,43 @@ namespace Emby.Server.Implementations.Data
             return result[index].ToInt64();
         }
 
-        public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool TryGetLong(this IReadOnlyList<IResultSetValue> reader, int index, out long? result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToInt64();
+            return true;
+        }
+
+        public static bool TryGetFloat(this IReadOnlyList<IResultSetValue> reader, int index, out float? result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToFloat();
+            return true;
+        }
+
+        public static bool TryGetDouble(this IReadOnlyList<IResultSetValue> reader, int index, out double? result)
         {
-            return result[index].ToFloat();
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToDouble();
+            return true;
         }
 
         public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 219 - 330
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 8 - 8
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -355,9 +355,9 @@ namespace Emby.Server.Implementations.Data
             userData.Key = reader[0].ToString();
             // userData.UserId = reader[1].ReadGuidFromBlob();
 
-            if (reader[2].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetDouble(2, out var rating))
             {
-                userData.Rating = reader[2].ToDouble();
+                userData.Rating = rating;
             }
 
             userData.Played = reader[3].ToBool();
@@ -365,19 +365,19 @@ namespace Emby.Server.Implementations.Data
             userData.IsFavorite = reader[5].ToBool();
             userData.PlaybackPositionTicks = reader[6].ToInt64();
 
-            if (reader[7].SQLiteType != SQLiteType.Null)
+            if (reader.TryReadDateTime(7, out var lastPlayedDate))
             {
-                userData.LastPlayedDate = reader[7].TryReadDateTime();
+                userData.LastPlayedDate = lastPlayedDate;
             }
 
-            if (reader[8].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetInt(8, out var audioStreamIndex))
             {
-                userData.AudioStreamIndex = reader[8].ToInt();
+                userData.AudioStreamIndex = audioStreamIndex;
             }
 
-            if (reader[9].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetInt(9, out var subtitleStreamIndex))
             {
-                userData.SubtitleStreamIndex = reader[9].ToInt();
+                userData.SubtitleStreamIndex = subtitleStreamIndex;
             }
 
             return userData;

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

@@ -2,8 +2,8 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Net;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
@@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 auth = httpReq.Request.Headers[HeaderNames.Authorization];
             }
 
-            return GetAuthorization(auth);
+            return GetAuthorization(auth.Count > 0 ? auth[0] : null);
         }
 
         /// <summary>
@@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 auth = httpReq.Headers[HeaderNames.Authorization];
             }
 
-            return GetAuthorization(auth);
+            return GetAuthorization(auth.Count > 0 ? auth[0] : null);
         }
 
         /// <summary>
@@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="authorizationHeader">The authorization header.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorization(string authorizationHeader)
+        private Dictionary<string, string> GetAuthorization(ReadOnlySpan<char> authorizationHeader)
         {
             if (authorizationHeader == null)
             {
                 return null;
             }
 
-            var parts = authorizationHeader.Split(' ', 2);
+            var firstSpace = authorizationHeader.IndexOf(' ');
 
-            // There should be at least to parts
-            if (parts.Length != 2)
+            // There should be at least two parts
+            if (firstSpace == -1)
             {
                 return null;
             }
 
-            var acceptedNames = new[] { "MediaBrowser", "Emby" };
+            var name = authorizationHeader[..firstSpace];
 
-            // It has to be a digest request
-            if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
+            if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
+                && name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
             {
                 return null;
             }
 
-            // Remove uptil the first space
-            authorizationHeader = parts[1];
-            parts = authorizationHeader.Split(',');
+            authorizationHeader = authorizationHeader[(firstSpace + 1)..];
 
             var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            foreach (var item in parts)
+            foreach (var item in authorizationHeader.Split(','))
             {
-                var param = item.Trim().Split('=', 2);
+                var trimmedItem = item.Trim();
+                var firstEqualsSign = trimmedItem.IndexOf('=');
 
-                if (param.Length == 2)
+                if (firstEqualsSign > 0)
                 {
-                    var value = NormalizeValue(param[1].Trim('"'));
-                    result[param[0]] = value;
+                    var key = trimmedItem[..firstEqualsSign].ToString();
+                    var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
+                    result[key] = value;
                 }
             }
 

+ 8 - 8
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -165,13 +165,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
         protected void SetVideoType(Video video, VideoFileInfo videoInfo)
         {
-            var extension = Path.GetExtension(video.Path);
-            video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
-              VideoType.Iso :
-              VideoType.VideoFile;
+            var extension = Path.GetExtension(video.Path.AsSpan());
+            video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
+                              || extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
+                ? VideoType.Iso
+                : VideoType.VideoFile;
 
-            video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+            video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
             video.IsPlaceHolder = videoInfo.IsStub;
 
             if (videoInfo.IsStub)
@@ -193,11 +193,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
         {
             if (video.VideoType == VideoType.Iso)
             {
-                if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+                if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
                 {
                     video.IsoType = IsoType.Dvd;
                 }
-                else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+                else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
                 {
                     video.IsoType = IsoType.BluRay;
                 }

+ 20 - 21
Emby.Server.Implementations/Security/AuthenticationRepository.cs

@@ -297,50 +297,49 @@ namespace Emby.Server.Implementations.Security
                 AccessToken = reader[1].ToString()
             };
 
-            if (reader[2].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(2, out var deviceId))
             {
-                info.DeviceId = reader[2].ToString();
+                info.DeviceId = deviceId;
             }
 
-            if (reader[3].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(3, out var appName))
             {
-                info.AppName = reader[3].ToString();
+                info.AppName = appName;
             }
 
-            if (reader[4].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(4, out var appVersion))
             {
-                info.AppVersion = reader[4].ToString();
+                info.AppVersion = appVersion;
             }
 
-            if (reader[5].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(6, out var userId))
             {
-                info.DeviceName = reader[5].ToString();
+                info.UserId = new Guid(userId);
             }
 
-            if (reader[6].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(7, out var userName))
             {
-                info.UserId = new Guid(reader[6].ToString());
-            }
-
-            if (reader[7].SQLiteType != SQLiteType.Null)
-            {
-                info.UserName = reader[7].ToString();
+                info.UserName = userName;
             }
 
             info.DateCreated = reader[8].ReadDateTime();
 
-            if (reader[9].SQLiteType != SQLiteType.Null)
+            if (reader.TryReadDateTime(9, out var dateLastActivity))
             {
-                info.DateLastActivity = reader[9].ReadDateTime();
+                info.DateLastActivity = dateLastActivity.Value;
             }
             else
             {
                 info.DateLastActivity = info.DateCreated;
             }
 
-            if (reader[10].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetString(10, out var customName))
+            {
+                info.DeviceName = customName;
+            }
+            else if (reader.TryGetString(5, out var deviceName))
             {
-                info.DeviceName = reader[10].ToString();
+                info.DeviceName = deviceName;
             }
 
             return info;
@@ -361,9 +360,9 @@ namespace Emby.Server.Implementations.Security
 
                         foreach (var row in statement.ExecuteQuery())
                         {
-                            if (row[0].SQLiteType != SQLiteType.Null)
+                            if (row.TryGetString(0, out var customName))
                             {
-                                result.CustomName = row[0].ToString();
+                                result.CustomName = customName;
                             }
                         }
 

+ 11 - 0
Jellyfin.sln

@@ -81,6 +81,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Tests", "te
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Providers.Tests", "tests\Jellyfin.Providers.Tests\Jellyfin.Providers.Tests.csproj", "{A964008C-2136-4716-B6CB-B3426C22320A}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -223,6 +225,14 @@ Global
 		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{50928738-D268-43A3-BACA-3513D9AA6B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{50928738-D268-43A3-BACA-3513D9AA6B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{50928738-D268-43A3-BACA-3513D9AA6B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{50928738-D268-43A3-BACA-3513D9AA6B8E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -240,6 +250,7 @@ Global
 		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{A964008C-2136-4716-B6CB-B3426C22320A} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

+ 51 - 0
MediaBrowser.Common/Extensions/EnumerableExtensions.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Static extensions for the <see cref="IEnumerable{T}"/> interface.
+    /// </summary>
+    public static class EnumerableExtensions
+    {
+        /// <summary>
+        /// Determines whether the value is contained in the source collection.
+        /// </summary>
+        /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
+        /// <param name="value">The value to look for in the collection.</param>
+        /// <param name="stringComparison">The string comparison.</param>
+        /// <returns>A value indicating whether the value is contained in the collection.</returns>
+        /// <exception cref="ArgumentNullException">The source is null.</exception>
+        public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
+        {
+            if (source == null)
+            {
+                throw new ArgumentNullException(nameof(source));
+            }
+
+            if (source is IList<string> list)
+            {
+                int len = list.Count;
+                for (int i = 0; i < len; i++)
+                {
+                    if (value.Equals(list[i], stringComparison))
+                    {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+
+            foreach (string element in source)
+            {
+                if (value.Equals(element, stringComparison))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+}

+ 14 - 3
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -466,7 +466,7 @@ namespace MediaBrowser.LocalMetadata.Images
             return added;
         }
 
-        private bool AddImage(IEnumerable<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type)
+        private bool AddImage(List<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type)
         {
             var image = GetImage(files, name);
 
@@ -484,9 +484,20 @@ namespace MediaBrowser.LocalMetadata.Images
             return false;
         }
 
-        private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name)
+        private static FileSystemMetadata? GetImage(IReadOnlyList<FileSystemMetadata> files, string name)
         {
-            return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
+            for (var i = 0; i < files.Count; i++)
+            {
+                var file = files[i];
+                if (!file.IsDirectory
+                    && file.Length > 0
+                    && Path.GetFileNameWithoutExtension(file.FullName.AsSpan()).Equals(name, StringComparison.OrdinalIgnoreCase))
+                {
+                    return file;
+                }
+            }
+
+            return null;
         }
     }
 }

+ 67 - 68
MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs

@@ -15,17 +15,6 @@ namespace MediaBrowser.Providers.MediaInfo
     {
         private readonly ILocalizationManager _localization;
 
-        private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            ".srt",
-            ".ssa",
-            ".ass",
-            ".sub",
-            ".smi",
-            ".sami",
-            ".vtt"
-        };
-
         public SubtitleResolver(ILocalizationManager localization)
         {
             _localization = localization;
@@ -88,80 +77,65 @@ namespace MediaBrowser.Providers.MediaInfo
             return list;
         }
 
-        private void AddExternalSubtitleStreams(
-            List<MediaStream> streams,
-            string folder,
-            string videoPath,
-            int startIndex,
-            IDirectoryService directoryService,
-            bool clearCache)
-        {
-            var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray();
-
-            AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
-        }
-
         public void AddExternalSubtitleStreams(
             List<MediaStream> streams,
             string videoPath,
             int startIndex,
             string[] files)
         {
-            var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(videoPath);
-            videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoFileNameWithoutExtension);
+            var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
 
             foreach (var fullName in files)
             {
-                var extension = Path.GetExtension(fullName);
-
-                if (!SubtitleExtensions.Contains(extension))
+                var extension = Path.GetExtension(fullName.AsSpan());
+                if (!IsSubtitleExtension(extension))
                 {
                     continue;
                 }
 
-                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName);
-                fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fileNameWithoutExtension);
+                var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName);
 
-                if (!string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) &&
-                    !fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
-
-                var codec = Path.GetExtension(fullName).ToLowerInvariant().TrimStart('.');
-
-                if (string.Equals(codec, "txt", StringComparison.OrdinalIgnoreCase))
-                {
-                    codec = "srt";
-                }
+                MediaStream mediaStream;
 
-                // If the subtitle file matches the video file name
-                if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+                // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot
+                if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
                 {
-                    streams.Add(new MediaStream
+                    mediaStream = new MediaStream
                     {
                         Index = startIndex++,
                         Type = MediaStreamType.Subtitle,
                         IsExternal = true,
-                        Path = fullName,
-                        Codec = codec
-                    });
+                        Path = fullName
+                    };
                 }
-                else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
+                else if (fileNameWithoutExtension.Length >= videoFileNameWithoutExtension.Length
+                         && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+                         && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
                 {
-                    var isForced = fullName.IndexOf(".forced.", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        fullName.IndexOf(".foreign.", StringComparison.OrdinalIgnoreCase) != -1;
+                    var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase)
+                                   || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase);
 
-                    var isDefault = fullName.IndexOf(".default.", StringComparison.OrdinalIgnoreCase) != -1;
+                    var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase);
 
                     // Support xbmc naming conventions - 300.spanish.srt
-                    var language = fileNameWithoutExtension
-                        .Replace(".forced", string.Empty, StringComparison.OrdinalIgnoreCase)
-                        .Replace(".foreign", string.Empty, StringComparison.OrdinalIgnoreCase)
-                        .Replace(".default", string.Empty, StringComparison.OrdinalIgnoreCase)
-                        .Split('.')
-                        .LastOrDefault();
+                    var languageSpan = fileNameWithoutExtension;
+                    while (languageSpan.Length > 0)
+                    {
+                        var lastDot = languageSpan.LastIndexOf('.');
+                        var currentSlice = languageSpan[lastDot..];
+                        if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
+                            || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
+                            || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase))
+                        {
+                            languageSpan = languageSpan[..lastDot];
+                            continue;
+                        }
+
+                        languageSpan = languageSpan[(lastDot + 1)..];
+                        break;
+                    }
 
+                    var language = languageSpan.ToString();
                     // Try to translate to three character code
                     // Be flexible and check against both the full and three character versions
                     var culture = _localization.FindLanguageInfo(language);
@@ -171,33 +145,58 @@ namespace MediaBrowser.Providers.MediaInfo
                         language = culture.ThreeLetterISOLanguageName;
                     }
 
-                    streams.Add(new MediaStream
+                    mediaStream = new MediaStream
                     {
                         Index = startIndex++,
                         Type = MediaStreamType.Subtitle,
                         IsExternal = true,
                         Path = fullName,
-                        Codec = codec,
                         Language = language,
                         IsForced = isForced,
                         IsDefault = isDefault
-                    });
+                    };
+                }
+                else
+                {
+                    continue;
                 }
+
+                mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
+
+                streams.Add(mediaStream);
             }
         }
 
-        private string NormalizeFilenameForSubtitleComparison(string filename)
+        private static bool IsSubtitleExtension(ReadOnlySpan<char> extension)
+        {
+            return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase)
+                   || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase);
+        }
+
+        private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename)
         {
             // Try to account for sloppy file naming
             filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
             filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
+            return Path.GetFileNameWithoutExtension(filename.AsSpan());
+        }
 
-            // can't normalize this due to languages such as pt-br
-            // filename = filename.Replace("-", string.Empty);
-
-            // filename = filename.Replace(".", string.Empty);
+        private void AddExternalSubtitleStreams(
+            List<MediaStream> streams,
+            string folder,
+            string videoPath,
+            int startIndex,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray();
 
-            return filename;
+            AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
         }
     }
 }

+ 37 - 0
tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj

@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <IsPackable>false</IsPackable>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Moq" Version="4.16.1" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="1.3.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <!-- Code Analyzers -->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../../MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
+  </ItemGroup>
+
+</Project>

+ 96 - 0
tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs

@@ -0,0 +1,96 @@
+#pragma warning disable CA1002 // Do not expose generic lists
+
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+    public class SubtitleResolverTests
+    {
+        public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
+        {
+            var index = 0;
+            yield return new object[]
+            {
+                new List<MediaStream>(),
+                "/video/My.Video.mkv",
+                index,
+                new[]
+                {
+                    "/video/My.Video.mp3",
+                    "/video/My.Video.png",
+                    "/video/My.Video.srt",
+                    "/video/My.Video.txt",
+                    "/video/My.Video.vtt",
+                    "/video/My.Video.ass",
+                    "/video/My.Video.sub",
+                    "/video/My.Video.ssa",
+                    "/video/My.Video.smi",
+                    "/video/My.Video.sami",
+                    "/video/My.Video.en.srt",
+                    "/video/My.Video.default.en.srt",
+                    "/video/My.Video.default.forced.en.srt",
+                    "/video/My.Video.en.default.forced.srt",
+                    "/video/My.Video.With.Additional.Garbage.en.srt",
+                    "/video/My.Video With Additional Garbage.srt"
+                },
+                new List<MediaStream>
+                {
+                    CreateMediaStream("/video/My.Video.srt", "srt", null, index++),
+                    CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++),
+                    CreateMediaStream("/video/My.Video.ass", "ass", null, index++),
+                    CreateMediaStream("/video/My.Video.sub", "sub", null, index++),
+                    CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++),
+                    CreateMediaStream("/video/My.Video.smi", "smi", null, index++),
+                    CreateMediaStream("/video/My.Video.sami", "sami", null, index++),
+                    CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++),
+                    CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true),
+                    CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
+                    CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
+                    CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
+                }
+            };
+        }
+
+        [Theory]
+        [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))]
+        public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, List<MediaStream> expectedResult)
+        {
+            new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
+
+            Assert.Equal(expectedResult.Count, streams.Count);
+            for (var i = 0; i < expectedResult.Count; i++)
+            {
+                var expected = expectedResult[i];
+                var actual = streams[i];
+
+                Assert.Equal(expected.Index, actual.Index);
+                Assert.Equal(expected.Type, actual.Type);
+                Assert.Equal(expected.IsExternal, actual.IsExternal);
+                Assert.Equal(expected.Path, actual.Path);
+                Assert.Equal(expected.IsDefault, actual.IsDefault);
+                Assert.Equal(expected.IsForced, actual.IsForced);
+                Assert.Equal(expected.Language, actual.Language);
+            }
+        }
+
+        private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
+        {
+            return new ()
+            {
+                Index = index,
+                Codec = codec,
+                Type = MediaStreamType.Subtitle,
+                IsExternal = true,
+                Path = path,
+                IsDefault = isDefault,
+                IsForced = isForced,
+                Language = language
+            };
+        }
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно