Reduce some allocations

pull/6054/head
cvium 3 years ago
parent 5acb4e9491
commit 1b49435a0e

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

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

@ -29,70 +29,73 @@ namespace Emby.Naming.Video
/// <param name="path">Path to file.</param> /// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns> /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path) 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(); 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)
else if (rule.MediaType == MediaType.Video)
{
if (!new VideoResolver(_options).IsVideoFile(path))
{ {
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; var filename = Path.GetFileNameWithoutExtension(pathSpan);
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
var filename = Path.GetFileNameWithoutExtension(path);
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; var filename = Path.GetFileNameWithoutExtension(pathSpan);
result.Rule = rule;
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
} }
} else if (rule.RuleType == ExtraRuleType.Regex)
else if (rule.RuleType == ExtraRuleType.Regex) {
{ var filename = Path.GetFileName(path);
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; var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
result.Rule = rule; if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
} }
}
else if (rule.RuleType == ExtraRuleType.DirectoryName) if (result.ExtraType != null)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
{ {
result.ExtraType = rule.ExtraType; return result;
result.Rule = rule;
} }
} }

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

@ -181,11 +181,9 @@ namespace Emby.Server.Implementations.Data
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) 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(columnName);
columnNames.Add(name);
} }
} }

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -96,21 +97,42 @@ namespace Emby.Server.Implementations.Data
DateTimeStyles.None).ToUniversalTime(); 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)) 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) public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
@ -118,14 +140,48 @@ namespace Emby.Server.Implementations.Data
return result[index].ToString(); 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) public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
{ {
return result[index].ToBool(); 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) public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
@ -133,9 +189,43 @@ namespace Emby.Server.Implementations.Data
return result[index].ToInt64(); 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) public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)

File diff suppressed because it is too large Load Diff

@ -355,9 +355,9 @@ namespace Emby.Server.Implementations.Data
userData.Key = reader[0].ToString(); userData.Key = reader[0].ToString();
// userData.UserId = reader[1].ReadGuidFromBlob(); // 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(); userData.Played = reader[3].ToBool();
@ -365,19 +365,19 @@ namespace Emby.Server.Implementations.Data
userData.IsFavorite = reader[5].ToBool(); userData.IsFavorite = reader[5].ToBool();
userData.PlaybackPositionTicks = reader[6].ToInt64(); 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; return userData;

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

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

@ -297,50 +297,49 @@ namespace Emby.Server.Implementations.Security
AccessToken = reader[1].ToString() 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()); info.UserName = userName;
}
if (reader[7].SQLiteType != SQLiteType.Null)
{
info.UserName = reader[7].ToString();
} }
info.DateCreated = reader[8].ReadDateTime(); 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 else
{ {
info.DateLastActivity = info.DateCreated; 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; return info;
@ -361,9 +360,9 @@ namespace Emby.Server.Implementations.Security
foreach (var row in statement.ExecuteQuery()) 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;
} }
} }

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

@ -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;
}
}
}

@ -466,7 +466,7 @@ namespace MediaBrowser.LocalMetadata.Images
return added; 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); var image = GetImage(files, name);
@ -484,9 +484,20 @@ namespace MediaBrowser.LocalMetadata.Images
return false; 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;
} }
} }
} }

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

@ -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>

@ -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
};
}
}
}
Loading…
Cancel
Save