Reduce some allocations

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

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

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

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

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

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

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

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.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;

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

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

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

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

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

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

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