From 0f897589ed6349bb3c88919b06861daa80aec1be Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 21 May 2019 19:28:34 +0200 Subject: Streamline authentication proccess --- Emby.Server.Implementations/Library/UserManager.cs | 49 ++++++++++++---------- 1 file changed, 27 insertions(+), 22 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 1701ced42..c8c8a108d 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -266,6 +266,7 @@ namespace Emby.Server.Implementations.Library builder.Append(c); } } + return builder.ToString(); } @@ -286,17 +287,17 @@ namespace Emby.Server.Implementations.Library if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + updatedUsername = authResult.username; + success = authResult.success; } else { // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + updatedUsername = authResult.username; + success = authResult.success; if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) { @@ -331,22 +332,25 @@ namespace Emby.Server.Implementations.Library if (user == null) { - throw new SecurityException("Invalid username or password entered."); + throw new AuthenticationException("Invalid username or password entered."); } if (user.Policy.IsDisabled) { - throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + throw new AuthenticationException(string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { - throw new SecurityException("Forbidden."); + throw new AuthenticationException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { - throw new SecurityException("User is not allowed access at this time."); + throw new AuthenticationException("User is not allowed access at this time."); } // Update LastActivityDate and LastLoginDate, then save @@ -357,6 +361,7 @@ namespace Emby.Server.Implementations.Library user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; UpdateUser(user); } + UpdateInvalidLoginAttemptCount(user, 0); } else @@ -429,7 +434,7 @@ namespace Emby.Server.Implementations.Library return providers; } - private async Task> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + private async Task<(string username, bool success)> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try { @@ -444,23 +449,23 @@ namespace Emby.Server.Implementations.Library authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); } - if(authenticationResult.Username != username) + if (authenticationResult.Username != username) { _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); username = authenticationResult.Username; } - return new Tuple(username, true); + return (username, true); } - catch (Exception ex) + catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - return new Tuple(username, false); + return (username, false); } } - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { string updatedUsername = null; bool success = false; @@ -475,15 +480,15 @@ namespace Emby.Server.Implementations.Library if (password == null) { // legacy - success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); } else { foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.Item1; - success = providerAuthResult.Item2; + updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; if (success) { @@ -510,7 +515,7 @@ namespace Emby.Server.Implementations.Library } } - return new Tuple(authenticationProvider, username, success); + return (authenticationProvider, username, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) @@ -593,7 +598,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? -- cgit v1.2.3 From 8d3b5c851ded74b9f34eb2cd963187761a3f6f61 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 9 Jun 2019 22:08:01 +0200 Subject: Improvements to UserManager --- Emby.Notifications/NotificationManager.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 3 +- .../Data/SqliteUserDataRepository.cs | 4 +- .../EntryPoints/RefreshUsersMetadata.cs | 4 +- .../Library/DefaultPasswordResetProvider.cs | 44 +-- Emby.Server.Implementations/Library/UserManager.cs | 358 ++++++++++----------- .../Session/SessionManager.cs | 8 +- MediaBrowser.Api/UserService.cs | 11 +- .../Authentication/AuthenticationException.cs | 1 + MediaBrowser.Controller/Entities/User.cs | 66 +--- MediaBrowser.Controller/Library/IUserManager.cs | 10 +- 11 files changed, 237 insertions(+), 274 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index a767e541e..eecbbea07 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -89,7 +89,7 @@ namespace Emby.Notifications return _userManager.Users.Where(i => i.Policy.IsAdministrator) .Select(i => i.Id); case SendToUserType.All: - return _userManager.Users.Select(i => i.Id); + return _userManager.UsersIds; case SendToUserType.Custom: return request.UserIds; default: diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0b3b81f94..c390ba635 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -770,7 +770,8 @@ namespace Emby.Server.Implementations _userRepository = GetUserRepository(); - UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + UserManager = new UserManager(LoggerFactory.CreateLogger(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4035bb99d..9d4855bcf 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users.ToArray(); + var users = userDatasTableExists ? null : userManager.Users; connection.RunInTransaction(db => { @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Data } } - private void ImportUserIds(IDatabaseConnection db, User[] users) + private void ImportUserIds(IDatabaseConnection db, IEnumerable users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b7565adec..b2328121e 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -50,9 +50,7 @@ namespace Emby.Server.Implementations.EntryPoints public async Task Execute(CancellationToken cancellationToken, IProgress progress) { - var users = _userManager.Users.ToList(); - - foreach (var user in users) + foreach (var user in _userManager.Users) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index c7044820c..fa6bbcf91 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; +using System.Security.Cryptography; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; @@ -17,32 +14,37 @@ namespace Emby.Server.Implementations.Library { public class DefaultPasswordResetProvider : IPasswordResetProvider { - public string Name => "Default Password Reset Provider"; + private const string BaseResetFileName = "passwordreset"; - public bool IsEnabled => true; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - private readonly ICryptoProvider _crypto; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); _jsonSerializer = jsonSerializer; _userManager = userManager; - _crypto = cryptoProvider; } + /// + public string Name => "Default Password Reset Provider"; + + /// + public bool IsEnabled => true; + + /// public async Task RedeemPasswordResetPin(string pin) { SerializablePasswordReset spr; - HashSet usersreset = new HashSet(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + List usersreset = new List(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { using (var str = File.OpenRead(resetfile)) { @@ -53,12 +55,15 @@ namespace Emby.Server.Implementations.Library { File.Delete(resetfile); } - else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) + else if (string.Equals( + spr.Pin.Replace("-", string.Empty), + pin.Replace("-", string.Empty), + StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) { - throw new Exception($"User with a username of {spr.UserName} not found"); + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); } await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); @@ -81,10 +86,11 @@ namespace Emby.Server.Implementations.Library } } + /// public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { string pin = string.Empty; - using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) + using (var cryptoRandom = RandomNumberGenerator.Create()) { byte[] bytes = new byte[4]; cryptoRandom.GetBytes(bytes); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index c8c8a108d..086527883 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,13 +12,11 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; @@ -40,35 +39,20 @@ namespace Emby.Server.Implementations.Library /// public class UserManager : IUserManager { - /// - /// Gets the users. - /// - /// The users. - public IEnumerable Users => _users; - - private User[] _users; - /// /// The _logger /// private readonly ILogger _logger; - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly object _policySyncLock = new object(); /// /// Gets the active user repository /// /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - + private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; private readonly Func _imageProcessorFactory; @@ -76,6 +60,8 @@ namespace Emby.Server.Implementations.Library private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private ConcurrentDictionary _users; + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; @@ -85,8 +71,7 @@ namespace Emby.Server.Implementations.Library private DefaultPasswordResetProvider _defaultPasswordResetProvider; public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, + ILogger logger, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, @@ -96,8 +81,8 @@ namespace Emby.Server.Implementations.Library IJsonSerializer jsonSerializer, IFileSystem fileSystem) { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; + _logger = logger; + _userRepository = userRepository; _xmlSerializer = xmlSerializer; _networkManager = networkManager; _imageProcessorFactory = imageProcessorFactory; @@ -105,8 +90,51 @@ namespace Emby.Server.Implementations.Library _appHost = appHost; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty(); + _users = null; + } + + public event EventHandler> UserPasswordChanged; + + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + + public event EventHandler> UserPolicyUpdated; + + public event EventHandler> UserConfigurationUpdated; + + public event EventHandler> UserLockedOut; + + public event EventHandler> UserCreated; + + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + + /// + public IEnumerable Users => _users.Values; + + /// + public IEnumerable UsersIds => _users.Keys; + + /// + /// Called when [user updated]. + /// + /// The user. + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + + /// + /// Called when [user deleted]. + /// + /// The user. + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); } public NameIdPair[] GetAuthenticationProviders() @@ -137,7 +165,7 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable authenticationProviders,IEnumerable passwordResetProviders) + public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); @@ -150,54 +178,21 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); } - #region UserUpdated Event /// - /// Occurs when [user updated]. - /// - public event EventHandler> UserUpdated; - public event EventHandler> UserPolicyUpdated; - public event EventHandler> UserConfigurationUpdated; - public event EventHandler> UserLockedOut; - - /// - /// Called when [user updated]. - /// - /// The user. - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// - /// Occurs when [user deleted]. - /// - public event EventHandler> UserDeleted; - /// - /// Called when [user deleted]. - /// - /// The user. - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - /// - /// Gets a User by Id + /// Gets a User by Id. /// /// The id. /// User. - /// + /// public User GetUserById(Guid id) { if (id == Guid.Empty) { - throw new ArgumentException(nameof(id), "Guid can't be empty"); + throw new ArgumentException("Guid can't be empty", nameof(id)); } - return Users.FirstOrDefault(u => u.Id == id); + _users.TryGetValue(id, out User user); + return user; } /// @@ -206,15 +201,13 @@ namespace Emby.Server.Implementations.Library /// The identifier. /// User. public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } + => GetUserById(new Guid(id)); public User GetUserByName(string name) { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException("Invalid username", nameof(name)); } return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); @@ -222,8 +215,9 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - var users = LoadUsers(); - _users = users.ToArray(); + LoadUsers(); + + var users = Users; // If there are no local users with admin rights, make them all admins if (!users.Any(i => i.Policy.IsAdministrator)) @@ -240,14 +234,12 @@ namespace Emby.Server.Implementations.Library { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } + => IsValidUsername(i.ToString(CultureInfo.InvariantCulture)); public string MakeValidUsername(string username) { @@ -277,8 +269,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(username)); } - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; string updatedUsername = null; @@ -299,13 +290,12 @@ namespace Emby.Server.Implementations.Library updatedUsername = authResult.username; success = authResult.success; - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) { // We should trust the user that the authprovider says, not what was typed - if (updatedUsername != username) - { - username = updatedUsername; - } + username = updatedUsername; // Search the database for the user again; the authprovider might have created it user = Users @@ -337,10 +327,11 @@ namespace Emby.Server.Implementations.Library if (user.Policy.IsDisabled) { - throw new AuthenticationException(string.Format( - CultureInfo.InvariantCulture, - "The {0} account is currently disabled. Please consult with your administrator.", - user.Name)); + throw new AuthenticationException( + string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) @@ -386,7 +377,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider GetAuthenticationProvider(User user) { - return GetAuthenticationProviders(user).First(); + return GetAuthenticationProviders(user)[0]; } private IPasswordResetProvider GetPasswordResetProvider(User user) @@ -396,7 +387,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] GetAuthenticationProviders(User user) { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + var authenticationProviderId = user?.Policy.AuthenticationProviderId; var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); @@ -438,16 +429,10 @@ namespace Emby.Server.Implementations.Library { try { - var requiresResolvedUser = provider as IRequiresResolvedUser; - ProviderAuthenticationResult authenticationResult = null; - if (requiresResolvedUser != null) - { - authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); - } + + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); if (authenticationResult.Username != username) { @@ -467,7 +452,6 @@ namespace Emby.Server.Implementations.Library private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { - string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; @@ -487,7 +471,7 @@ namespace Emby.Server.Implementations.Library foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.username; + var updatedUsername = providerAuthResult.username; success = providerAuthResult.success; if (success) @@ -499,25 +483,32 @@ namespace Emby.Server.Implementations.Library } } - if (user != null) + if (user != null + && !success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user.Configuration.EnableLocalPassword) { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + if (password == null) { - if (password == null) - { - // legacy - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); } } return (authenticationProvider, username, success); } + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); + } + private void UpdateInvalidLoginAttemptCount(User user, int newValue) { if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) @@ -556,17 +547,17 @@ namespace Emby.Server.Implementations.Library } /// - /// Loads the users from the repository + /// Loads the users from the repository. /// - /// IEnumerable{User}. - private List LoadUsers() + private void LoadUsers() { - var users = UserRepository.RetrieveAllUsers(); + var users = _userRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count != 0) { - return users; + _users = new ConcurrentDictionary( + users.Select(x => new KeyValuePair(x.Id, x))); } var defaultName = Environment.UserName; @@ -581,14 +572,15 @@ namespace Emby.Server.Implementations.Library user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); user.Policy.IsAdministrator = true; user.Policy.EnableContentDeletion = true; user.Policy.EnableRemoteControlOfOtherUsers = true; UpdateUserPolicy(user, user.Policy, false); - return new List { user }; + _users = new ConcurrentDictionary(); + _users[user.Id] = user; } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -619,7 +611,7 @@ namespace Emby.Server.Implementations.Library Policy = user.Policy }; - if (!hasPassword && Users.Count() == 1) + if (!hasPassword && _users.Count == 1) { dto.EnableAutoLogin = true; } @@ -694,22 +686,26 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (string.IsNullOrEmpty(newName)) + if (string.IsNullOrWhiteSpace(newName)) { - throw new ArgumentNullException(nameof(newName)); + throw new ArgumentException("Invalid username", nameof(newName)); } - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + throw new ArgumentException("The new and old names must be different."); } - if (user.Name.Equals(newName, StringComparison.Ordinal)) + if (Users.Any( + u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException("The new and old names must be different."); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); } - await user.Rename(newName); + await user.Rename(newName).ConfigureAwait(false); OnUserUpdated(user); } @@ -727,23 +723,30 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id == Guid.Empty) { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + throw new ArgumentException("Id can't be empty.", nameof(user)); + } + + if (!_users.ContainsKey(user.Id)) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "A user '{0}' with Id {1} does not exist.", + user.Name, + user.Id), + nameof(user)); } user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.UpdateUser(user); + _userRepository.UpdateUser(user); OnUserUpdated(user); } - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - /// /// Creates the user. /// @@ -751,7 +754,7 @@ namespace Emby.Server.Implementations.Library /// User. /// name /// - public async Task CreateUser(string name) + public User CreateUser(string name) { if (string.IsNullOrWhiteSpace(name)) { @@ -768,28 +771,17 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name); - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); + _users[user.Id] = user; - user.DateLastSaved = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - return user; - } - finally - { - _userListLock.Release(); - } + return user; } /// @@ -799,57 +791,59 @@ namespace Emby.Server.Implementations.Library /// Task. /// user /// - public async Task DeleteUser(User user) + public void DeleteUser(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + if (!_users.ContainsKey(user.Id)) { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Name, + user.Id)); } - if (allUsers.Count == 1) + if (_users.Count == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Name)); } - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + if (user.Policy.IsAdministrator + && Users.Count(i => i.Policy.IsAdministrator) == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Name), + nameof(user)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + var configPath = GetConfigurationFilePath(user); + + _userRepository.DeleteUser(user); try { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); + _fileSystem.DeleteFile(configPath); } - finally + catch (IOException ex) { - _userListLock.Release(); + _logger.LogError(ex, "Error deleting file {path}", configPath); } + + DeleteUserPolicy(user); + + _users.TryRemove(user.Id, out _); + + OnUserDeleted(user); } /// @@ -906,8 +900,7 @@ namespace Emby.Server.Implementations.Library Name = name, Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true + DateModified = DateTime.UtcNow }; } @@ -989,7 +982,6 @@ namespace Emby.Server.Implementations.Library }; } - private readonly object _policySyncLock = new object(); public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 0347100a4..61329160a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1375,16 +1375,14 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (!request.UserId.Equals(Guid.Empty)) + if (request.UserId != Guid.Empty) { - user = _userManager.Users - .FirstOrDefault(i => i.Id == request.UserId); + user = _userManager.GetUserById(request.UserId); } if (user == null) { - user = _userManager.Users - .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); + user = _userManager.GetUserByName(request.Username); } if (user != null) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index fa70a52aa..21a94a4e0 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -365,8 +365,8 @@ namespace MediaBrowser.Api } _sessionMananger.RevokeUserTokens(user.Id, null); - - return _userManager.DeleteUser(user); + _userManager.DeleteUser(user); + return Task.CompletedTask; } /// @@ -503,9 +503,14 @@ namespace MediaBrowser.Api } } + /// + /// Posts the specified request. + /// + /// The request. + /// System.Object. public async Task Post(CreateUserByName request) { - var newUser = await _userManager.CreateUser(request.Name).ConfigureAwait(false); + var newUser = _userManager.CreateUser(request.Name); // no need to authenticate password for new user if (request.Password != null) diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs index 045cbcdae..62eca3ea9 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -1,4 +1,5 @@ using System; + namespace MediaBrowser.Controller.Authentication { /// diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 968d72579..7d245d4aa 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -17,13 +17,6 @@ namespace MediaBrowser.Controller.Entities public class User : BaseItem { public static IUserManager UserManager { get; set; } - public static IXmlSerializer XmlSerializer { get; set; } - - /// - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// - public bool UsesIdForConfigurationPath { get; set; } /// /// Gets or sets the password. @@ -31,7 +24,6 @@ namespace MediaBrowser.Controller.Entities /// The password. public string Password { get; set; } public string EasyPassword { get; set; } - public string Salt { get; set; } // Strictly to remove IgnoreDataMember public override ItemImageInfo[] ImageInfos @@ -148,46 +140,23 @@ namespace MediaBrowser.Controller.Entities /// public Task Rename(string newName) { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - // If only the casing is changing, leave the file system alone - if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(newName)) { - UsesIdForConfigurationPath = true; - - // Move configuration - var newConfigDirectory = GetConfigurationDirectoryPath(newName); - var oldConfigurationDirectory = ConfigurationDirectoryPath; - - // Exceptions will be thrown if these paths already exist - if (Directory.Exists(newConfigDirectory)) - { - Directory.Delete(newConfigDirectory, true); - } - - if (Directory.Exists(oldConfigurationDirectory)) - { - Directory.Move(oldConfigurationDirectory, newConfigDirectory); - } - else - { - Directory.CreateDirectory(newConfigDirectory); - } + throw new ArgumentException("Username can't be empty", nameof(newName)); } Name = newName; - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true + return RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + { + ReplaceAllMetadata = true, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = true - }, CancellationToken.None); + }, + CancellationToken.None); } public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) @@ -216,19 +185,6 @@ namespace MediaBrowser.Controller.Entities { var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - // Legacy - if (!UsesIdForConfigurationPath) - { - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var safeFolderName = FileSystem.GetValidFilename(username); - - return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); - } - // TODO: Remove idPath and just use usernamePath for future releases var usernamePath = System.IO.Path.Combine(parentPath, username); var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture)); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 7f7370893..bbedc0442 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -22,6 +22,12 @@ namespace MediaBrowser.Controller.Library /// The users. IEnumerable Users { get; } + /// + /// Gets the user ids. + /// + /// The users ids. + IEnumerable UsersIds { get; } + /// /// Occurs when [user updated]. /// @@ -92,7 +98,7 @@ namespace MediaBrowser.Controller.Library /// User. /// name /// - Task CreateUser(string name); + User CreateUser(string name); /// /// Deletes the user. @@ -101,7 +107,7 @@ namespace MediaBrowser.Controller.Library /// Task. /// user /// - Task DeleteUser(User user); + void DeleteUser(User user); /// /// Resets the password. -- cgit v1.2.3 From 24fac4b19172eae6a46208d712de09ac97e59d07 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 18 Aug 2019 20:12:25 +0200 Subject: Fix UserNotFoundError --- Emby.Server.Implementations/Library/UserManager.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 086527883..a7ea13ca6 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -558,6 +558,7 @@ namespace Emby.Server.Implementations.Library { _users = new ConcurrentDictionary( users.Select(x => new KeyValuePair(x.Id, x))); + return; } var defaultName = Environment.UserName; -- cgit v1.2.3 From 221b831bb23f73437594f2f760bb1e0700e77882 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 13 Sep 2019 17:16:33 +0200 Subject: Reset invalid login counter on successfull login --- Emby.Server.Implementations/Library/UserManager.cs | 47 ++++++++-------------- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- 2 files changed, 18 insertions(+), 31 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index a7ea13ca6..afa53ff37 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -353,11 +353,11 @@ namespace Emby.Server.Implementations.Library UpdateUser(user); } - UpdateInvalidLoginAttemptCount(user, 0); + ResetInvalidLoginAttemptCount(user); } else { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + IncrementInvalidLoginAttemptCount(user); } _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); @@ -509,41 +509,28 @@ namespace Emby.Server.Implementations.Library : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); } - private void UpdateInvalidLoginAttemptCount(User user, int newValue) + private void ResetInvalidLoginAttemptCount(User user) { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - // Check for users without a value here and then fill in the default value - // also protect from an always lockout if misconfigured - if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) - { - user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; - } - - var maxCount = user.Policy.LoginAttemptsBeforeLockout; - - var fireLockout = false; + user.Policy.InvalidLoginAttemptCount = 0; + UpdateUserPolicy(user, user.Policy, false); + } - // -1 can be used to specify no lockout value - if (maxCount != -1 && newValue >= maxCount) + private void IncrementInvalidLoginAttemptCount(User user) + { + int invalidLogins = ++user.Policy.InvalidLoginAttemptCount; + int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout; + if (maxInvalidLogins > 0 + && invalidLogins >= maxInvalidLogins) { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); user.Policy.IsDisabled = true; - - fireLockout = true; + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + _logger.LogWarning( + "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", + user.Name, + invalidLogins); } UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } } /// diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index f63ab2bb4..9336c720f 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Model.Users public bool EnableAllFolders { get; set; } public int InvalidLoginAttemptCount { get; set; } - public int? LoginAttemptsBeforeLockout { get; set; } + public int LoginAttemptsBeforeLockout { get; set; } public bool EnablePublicSharing { get; set; } -- cgit v1.2.3 From 6f17a0b7af5775386e554f2e2e2a4a6829d2895d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 17 Sep 2019 18:07:15 +0200 Subject: Remove legacy auth code (#1677) * Remove legacy auth code * Adds tests so we don't break PasswordHash (again) * Clean up interfaces * Remove duplicate code * Use auto properties * static using * Don't use 'this' * Fix build --- .../Cryptography/CryptographyProvider.cs | 65 +- .../Library/DefaultAuthenticationProvider.cs | 134 +- Emby.Server.Implementations/Library/UserManager.cs | 58 +- .../Updates/InstallationManager.cs | 4 +- MediaBrowser.Api/StartupWizardService.cs | 3 +- MediaBrowser.Common/Cryptography/Constants.cs | 18 + MediaBrowser.Common/Cryptography/Extensions.cs | 35 + MediaBrowser.Common/Cryptography/PasswordHash.cs | 155 +++ MediaBrowser.Common/Extensions/HexHelper.cs | 22 - MediaBrowser.Common/HexHelper.cs | 22 + MediaBrowser.Common/MediaBrowser.Common.csproj | 6 + MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 19 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 142 --- .../Resources/SampleTransformed.htm | 1277 -------------------- .../ConsistencyTests/Resources/StringCheck.xslt | 145 --- .../Resources/StringCheckSample.xml | 222 ---- .../ConsistencyTests/StringUsageReporter.cs | 259 ---- .../ConsistencyTests/TextIndexing/IndexBuilder.cs | 52 - .../ConsistencyTests/TextIndexing/WordIndex.cs | 36 - .../TextIndexing/WordOccurrence.cs | 18 - .../TextIndexing/WordOccurrences.cs | 13 - MediaBrowser.Tests/M3uParserTest.cs | 92 -- MediaBrowser.Tests/MediaBrowser.Tests.csproj | 139 --- .../MediaEncoding/Subtitles/AssParserTests.cs | 86 -- .../MediaEncoding/Subtitles/SrtParserTests.cs | 114 -- .../MediaEncoding/Subtitles/TestSubtitles/data.ass | 23 - .../Subtitles/TestSubtitles/data2.ass | 391 ------ .../Subtitles/TestSubtitles/expected.vtt | 32 - .../MediaEncoding/Subtitles/TestSubtitles/unit.srt | 44 - .../MediaEncoding/Subtitles/VttWriterTest.cs | 105 -- MediaBrowser.Tests/Properties/AssemblyInfo.cs | 23 - MediaBrowser.Tests/app.config | 11 - MediaBrowser.sln | 11 + .../Jellyfin.Common.Tests.csproj | 19 + tests/Jellyfin.Common.Tests/PasswordHashTests.cs | 29 + 35 files changed, 374 insertions(+), 3450 deletions(-) create mode 100644 MediaBrowser.Common/Cryptography/Constants.cs create mode 100644 MediaBrowser.Common/Cryptography/Extensions.cs create mode 100644 MediaBrowser.Common/Cryptography/PasswordHash.cs delete mode 100644 MediaBrowser.Common/Extensions/HexHelper.cs create mode 100644 MediaBrowser.Common/HexHelper.cs delete mode 100644 MediaBrowser.Model/Cryptography/PasswordHash.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml delete mode 100644 MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs delete mode 100644 MediaBrowser.Tests/M3uParserTest.cs delete mode 100644 MediaBrowser.Tests/MediaBrowser.Tests.csproj delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs delete mode 100644 MediaBrowser.Tests/Properties/AssemblyInfo.cs delete mode 100644 MediaBrowser.Tests/app.config create mode 100644 tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj create mode 100644 tests/Jellyfin.Common.Tests/PasswordHashTests.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index f726dae2e..23b77e268 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Security.Cryptography; -using System.Text; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; namespace Emby.Server.Implementations.Cryptography { @@ -30,8 +28,6 @@ namespace Emby.Server.Implementations.Cryptography private RandomNumberGenerator _randomNumberGenerator; - private const int _defaultIterations = 1000; - private bool _disposed = false; public CryptographyProvider() @@ -45,44 +41,13 @@ namespace Emby.Server.Implementations.Cryptography public string DefaultHashMethod => "PBKDF2"; - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public Guid GetMD5(string str) - => new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - - [Obsolete("Use System.Security.Cryptography.SHA1 directly")] - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } - public IEnumerable GetSupportedHashMethods() => _supportedHashMethods; private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) { - //downgrading for now as we need this library to be dotnetstandard compliant - //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment + // downgrading for now as we need this library to be dotnetstandard compliant + // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment if (method == DefaultHashMethod) { using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) @@ -104,7 +69,7 @@ namespace Emby.Server.Implementations.Cryptography { if (hashMethod == DefaultHashMethod) { - return PBKDF2(hashMethod, bytes, salt, _defaultIterations); + return PBKDF2(hashMethod, bytes, salt, DefaultIterations); } else if (_supportedHashMethods.Contains(hashMethod)) { @@ -129,26 +94,14 @@ namespace Emby.Server.Implementations.Cryptography } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - => PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); - - public byte[] ComputeHash(PasswordHash hash) - { - int iterations = _defaultIterations; - if (!hash.Parameters.ContainsKey("iterations")) - { - hash.Parameters.Add("iterations", iterations.ToString(CultureInfo.InvariantCulture)); - } - else if (!int.TryParse(hash.Parameters["iterations"], out iterations)) - { - throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}"); - } - - return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations); - } + => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations); public byte[] GenerateSalt() + => GenerateSalt(DefaultSaltLength); + + public byte[] GenerateSalt(int length) { - byte[] salt = new byte[64]; + byte[] salt = new byte[length]; _randomNumberGenerator.GetBytes(salt); return salt; } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 2282b8efb..c95b00ede 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -2,24 +2,30 @@ using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser { private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) { _cryptographyProvider = cryptographyProvider; } + /// public string Name => "Default"; + /// public bool IsEnabled => true; + /// // This is dumb and an artifact of the backwards way auth providers were designed. // This version of authenticate was never meant to be called, but needs to be here for interface compat // Only the providers that don't provide local user support use this @@ -28,6 +34,7 @@ namespace Emby.Server.Implementations.Library throw new NotImplementedException(); } + /// // This is the version that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { @@ -46,10 +53,9 @@ namespace Emby.Server.Implementations.Library }); } - ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - PasswordHash readyHash = new PasswordHash(resolvedUser.Password); + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { @@ -76,72 +82,31 @@ namespace Emby.Server.Implementations.Library }); } - // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change - // but at least they are in the new format. - private void ConvertPasswordFormat(User user) - { - if (string.IsNullOrEmpty(user.Password)) - { - return; - } - - if (user.Password.IndexOf('$') == -1) - { - string hash = user.Password; - user.Password = string.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null - && user.EasyPassword.IndexOf('$') == -1) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } - } - + /// public bool HasPassword(User user) => !string.IsNullOrEmpty(user.Password); + /// public Task ChangePassword(User user, string newPassword) { - ConvertPasswordFormat(user); - - // This is needed to support changing a no password user to a password user - if (string.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(newPassword)) { - PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); - newPasswordHash.Salt = _cryptographyProvider.GenerateSalt(); - newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; - newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash); - user.Password = newPasswordHash.ToString(); + user.Password = null; return Task.CompletedTask; } - PasswordHash passwordHash = new PasswordHash(user.Password); - if (passwordHash.Id == "SHA1" - && passwordHash.Salt.Length == 0) - { - passwordHash.Salt = _cryptographyProvider.GenerateSalt(); - passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash); - } - else if (newPassword != null) - { - passwordHash.Hash = GetHashed(user, newPassword); - } - - user.Password = passwordHash.ToString(); + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); return Task.CompletedTask; } + /// public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { - ConvertPasswordFormat(user); - if (newPassword != null) { - newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword)); + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); } if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -152,21 +117,12 @@ namespace Emby.Server.Implementations.Library user.EasyPassword = newPasswordHash; } + /// public string GetEasyPasswordHash(User user) { - // This should be removed in the future. This was added to let user login after - // Jellyfin 10.3.3 failed to save a well formatted PIN. - ConvertPasswordFormat(user); - return string.IsNullOrEmpty(user.EasyPassword) ? null - : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); - } - - internal byte[] GetHashedChangeAuth(string newPassword, PasswordHash passwordHash) - { - passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword); - return _cryptographyProvider.ComputeHash(passwordHash); + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); } /// @@ -174,54 +130,36 @@ namespace Emby.Server.Implementations.Library /// public string GetHashedString(User user, string str) { - PasswordHash passwordHash; if (string.IsNullOrEmpty(user.Password)) { - passwordHash = new PasswordHash(_cryptographyProvider); - } - else - { - ConvertPasswordFormat(user); - passwordHash = new PasswordHash(user.Password); + return _cryptographyProvider.CreatePasswordHash(str).ToString(); } - if (passwordHash.Salt != null) - { - // the password is modern format with PBKDF and we should take advantage of that - passwordHash.Hash = Encoding.UTF8.GetBytes(str); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); - } - else - { - // the password has no salt and should be called with the older method for safety - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); - } + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt), + passwordHash.Salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); } public byte[] GetHashed(User user, string str) { - PasswordHash passwordHash; if (string.IsNullOrEmpty(user.Password)) { - passwordHash = new PasswordHash(_cryptographyProvider); - } - else - { - ConvertPasswordFormat(user); - passwordHash = new PasswordHash(user.Password); + return _cryptographyProvider.CreatePasswordHash(str).Hash; } - if (passwordHash.Salt != null) - { - // the password is modern format with PBKDF and we should take advantage of that - passwordHash.Hash = Encoding.UTF8.GetBytes(str); - return _cryptographyProvider.ComputeHash(passwordHash); - } - else - { - // the password has no salt and should be called with the older method for safety - return _cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)); - } + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt); } } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index afa53ff37..ac6b4a209 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -23,7 +24,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -31,6 +31,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { @@ -450,53 +451,38 @@ namespace Emby.Server.Implementations.Library } } - private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser( + string username, + string password, + string hashedPassword, + User user, + string remoteEndPoint) { bool success = false; IAuthenticationProvider authenticationProvider = null; - if (password != null && user != null) + foreach (var provider in GetAuthenticationProviders(user)) { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } + var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; - if (password == null) - { - // legacy - success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) + if (success) { - var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - var updatedUsername = providerAuthResult.username; - success = providerAuthResult.success; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } + authenticationProvider = provider; + username = updatedUsername; + break; } } - if (user != null - && !success + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) { - if (password == null) - { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } + success = string.Equals( + GetLocalPasswordHash(user), + _defaultAuthenticationProvider.GetHashedString(user, password), + StringComparison.OrdinalIgnoreCase); } return (authenticationProvider, username, success); @@ -506,7 +492,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(user.EasyPassword) ? null - : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); } private void ResetInvalidLoginAttemptCount(User user) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 2f84b91ec..7947edeeb 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -19,6 +18,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Updates { @@ -454,7 +454,7 @@ namespace Emby.Server.Implementations.Updates { cancellationToken.ThrowIfCancellationRequested(); - var hash = HexHelper.ToHexString(md5.ComputeHash(stream)); + var hash = ToHexString(md5.ComputeHash(stream)); if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("{0}, {1}", package.checksum, hash); diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 53ba7eefd..3a9eb7a55 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -113,7 +113,8 @@ namespace MediaBrowser.Api _userManager.UpdateUser(user); - if (!string.IsNullOrEmpty(request.Password)) { + if (!string.IsNullOrEmpty(request.Password)) + { await _userManager.ChangePassword(user, request.Password).ConfigureAwait(false); } } diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Common/Cryptography/Constants.cs new file mode 100644 index 000000000..354114232 --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Constants.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Cryptography +{ + /// + /// Class containing global constants for Jellyfin Cryptography. + /// + public static class Constants + { + /// + /// The default length for new salts. + /// + public const int DefaultSaltLength = 64; + + /// + /// The default amount of iterations for hashing passwords. + /// + public const int DefaultIterations = 1000; + } +} diff --git a/MediaBrowser.Common/Cryptography/Extensions.cs b/MediaBrowser.Common/Cryptography/Extensions.cs new file mode 100644 index 000000000..1e32a6d1a --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Extensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; + +namespace MediaBrowser.Common.Cryptography +{ + /// + /// Class containing extension methods for working with Jellyfin cryptography objects. + /// + public static class Extensions + { + /// + /// Creates a new instance. + /// + /// The instance used. + /// The password that will be hashed. + /// A instance with the hash method, hash, salt and number of iterations. + public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password) + { + byte[] salt = cryptoProvider.GenerateSalt(); + return new PasswordHash( + cryptoProvider.DefaultHashMethod, + cryptoProvider.ComputeHashWithDefaultMethod( + Encoding.UTF8.GetBytes(password), + salt), + salt, + new Dictionary + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); + } + } +} diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs new file mode 100644 index 000000000..5b28d344f --- /dev/null +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using static MediaBrowser.Common.HexHelper; + +namespace MediaBrowser.Common.Cryptography +{ + // Defined from this hash storage spec + // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md + // $[$=(,=)*][$[$]] + // with one slight amendment to ease the transition, we're writing out the bytes in hex + // rather than making them a BASE64 string with stripped padding + public class PasswordHash + { + private readonly Dictionary _parameters; + + public PasswordHash(string id, byte[] hash) + : this(id, hash, Array.Empty()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt) + : this(id, hash, salt, new Dictionary()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary parameters) + { + Id = id; + Hash = hash; + Salt = salt; + _parameters = parameters; + } + + /// + /// Gets the symbolic name for the function used. + /// + /// Returns the symbolic name for the function used. + public string Id { get; } + + /// + /// Gets the additional parameters used by the hash function. + /// + /// + public IReadOnlyDictionary Parameters => _parameters; + + /// + /// Gets the salt used for hashing the password. + /// + /// Returns the salt used for hashing the password. + public byte[] Salt { get; } + + /// + /// Gets the hashed password. + /// + /// Return the hashed password. + public byte[] Hash { get; } + + public static PasswordHash Parse(string storageString) + { + string[] splitted = storageString.Split('$'); + // The string should at least contain the hash function and the hash itself + if (splitted.Length < 3) + { + throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); + } + + // Start at 1, the first index shouldn't contain any data + int index = 1; + + // Name of the hash function + string id = splitted[index++]; + + // Optional parameters + Dictionary parameters = new Dictionary(); + if (splitted[index].IndexOf('=') != -1) + { + foreach (string paramset in splitted[index++].Split(',')) + { + if (string.IsNullOrEmpty(paramset)) + { + continue; + } + + string[] fields = paramset.Split('='); + if (fields.Length != 2) + { + throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); + } + + parameters.Add(fields[0], fields[1]); + } + } + + byte[] hash; + byte[] salt; + // Check if the string also contains a salt + if (splitted.Length - index == 2) + { + salt = FromHexString(splitted[index++]); + hash = FromHexString(splitted[index++]); + } + else + { + salt = Array.Empty(); + hash = FromHexString(splitted[index++]); + } + + return new PasswordHash(id, hash, salt, parameters); + } + + private void SerializeParameters(StringBuilder stringBuilder) + { + if (_parameters.Count == 0) + { + return; + } + + stringBuilder.Append('$'); + foreach (var pair in _parameters) + { + stringBuilder.Append(pair.Key); + stringBuilder.Append('='); + stringBuilder.Append(pair.Value); + stringBuilder.Append(','); + } + + // Remove last ',' + stringBuilder.Length -= 1; + } + + /// + public override string ToString() + { + var str = new StringBuilder(); + str.Append('$'); + str.Append(Id); + SerializeParameters(str); + + if (Salt.Length != 0) + { + str.Append('$'); + str.Append(ToHexString(Salt)); + } + + str.Append('$'); + str.Append(ToHexString(Hash)); + + return str.ToString(); + } + } +} diff --git a/MediaBrowser.Common/Extensions/HexHelper.cs b/MediaBrowser.Common/Extensions/HexHelper.cs deleted file mode 100644 index 3d80d94ac..000000000 --- a/MediaBrowser.Common/Extensions/HexHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Globalization; - -namespace MediaBrowser.Common.Extensions -{ - public static class HexHelper - { - public static byte[] FromHexString(string str) - { - byte[] bytes = new byte[str.Length / 2]; - for (int i = 0; i < str.Length; i += 2) - { - bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - - return bytes; - } - - public static string ToHexString(byte[] bytes) - => BitConverter.ToString(bytes).Replace("-", ""); - } -} diff --git a/MediaBrowser.Common/HexHelper.cs b/MediaBrowser.Common/HexHelper.cs new file mode 100644 index 000000000..5587c03fd --- /dev/null +++ b/MediaBrowser.Common/HexHelper.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace MediaBrowser.Common +{ + public static class HexHelper + { + public static byte[] FromHexString(string str) + { + byte[] bytes = new byte[str.Length / 2]; + for (int i = 0; i < str.Length; i += 2) + { + bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return bytes; + } + + public static string ToHexString(byte[] bytes) + => BitConverter.ToString(bytes).Replace("-", ""); + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 91ab066f9..1a40f5ea2 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -31,4 +31,10 @@ latest + + + <_Parameter1>Jellyfin.Common.Tests + + + diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 9e85beb43..ce6493232 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography @@ -7,20 +5,19 @@ namespace MediaBrowser.Model.Cryptography public interface ICryptoProvider { string DefaultHashMethod { get; } - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - Guid GetMD5(string str); - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - byte[] ComputeMD5(Stream str); - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - byte[] ComputeMD5(byte[] bytes); - [Obsolete("Use System.Security.Cryptography.SHA1 directly")] - byte[] ComputeSHA1(byte[] bytes); + IEnumerable GetSupportedHashMethods(); + byte[] ComputeHash(string HashMethod, byte[] bytes); + byte[] ComputeHashWithDefaultMethod(byte[] bytes); + byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); + byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); - byte[] ComputeHash(PasswordHash hash); + byte[] GenerateSalt(); + + byte[] GenerateSalt(int length); } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs deleted file mode 100644 index 6e66f2088..000000000 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MediaBrowser.Model.Cryptography -{ - public class PasswordHash - { - // Defined from this hash storage spec - // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - // $[$=(,=)*][$[$]] - // with one slight amendment to ease the transition, we're writing out the bytes in hex - // rather than making them a BASE64 string with stripped padding - - private string _id; - - private Dictionary _parameters = new Dictionary(); - - private byte[] _salt; - - private byte[] _hash; - - public PasswordHash(string storageString) - { - string[] splitted = storageString.Split('$'); - // The string should at least contain the hash function and the hash itself - if (splitted.Length < 3) - { - throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); - } - - // Start at 1, the first index shouldn't contain any data - int index = 1; - - // Name of the hash function - _id = splitted[index++]; - - // Optional parameters - if (splitted[index].IndexOf('=') != -1) - { - foreach (string paramset in splitted[index++].Split(',')) - { - if (string.IsNullOrEmpty(paramset)) - { - continue; - } - - string[] fields = paramset.Split('='); - if (fields.Length != 2) - { - throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); - } - - _parameters.Add(fields[0], fields[1]); - } - } - - // Check if the string also contains a salt - if (splitted.Length - index == 2) - { - _salt = ConvertFromByteString(splitted[index++]); - _hash = ConvertFromByteString(splitted[index++]); - } - else - { - _salt = Array.Empty(); - _hash = ConvertFromByteString(splitted[index++]); - } - } - - public PasswordHash(ICryptoProvider cryptoProvider) - { - _id = cryptoProvider.DefaultHashMethod; - _salt = cryptoProvider.GenerateSalt(); - _hash = Array.Empty(); - } - - public string Id { get => _id; set => _id = value; } - - public Dictionary Parameters { get => _parameters; set => _parameters = value; } - - public byte[] Salt { get => _salt; set => _salt = value; } - - public byte[] Hash { get => _hash; set => _hash = value; } - - // TODO: move this class and use the HexHelper class - public static byte[] ConvertFromByteString(string byteString) - { - byte[] bytes = new byte[byteString.Length / 2]; - for (int i = 0; i < byteString.Length; i += 2) - { - // TODO: NetStandard2.1 switch this to use a span instead of a substring. - bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16); - } - - return bytes; - } - - public static string ConvertToByteString(byte[] bytes) - => BitConverter.ToString(bytes).Replace("-", string.Empty); - - private void SerializeParameters(StringBuilder stringBuilder) - { - if (_parameters.Count == 0) - { - return; - } - - stringBuilder.Append('$'); - foreach (var pair in _parameters) - { - stringBuilder.Append(pair.Key); - stringBuilder.Append('='); - stringBuilder.Append(pair.Value); - stringBuilder.Append(','); - } - - // Remove last ',' - stringBuilder.Length -= 1; - } - - public override string ToString() - { - var str = new StringBuilder(); - str.Append('$'); - str.Append(_id); - SerializeParameters(str); - - if (_salt.Length != 0) - { - str.Append('$'); - str.Append(ConvertToByteString(_salt)); - } - - str.Append('$'); - str.Append(ConvertToByteString(_hash)); - - return str.ToString(); - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm b/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm deleted file mode 100644 index f36652468..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm +++ /dev/null @@ -1,1277 +0,0 @@ - - - - - String Usage Report - - - -

String Usage Report

-
-

Strings

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- LabelExit: "
-
Exit"
-
-
-
-
- LabelVisitCommunity: "
-
Visit Community"
-
-
-
-
- LabelGithub: "
-
Github"
-
-
-
-
- LabelSwagger: "
-
Swagger"
-
-
-
-
- LabelStandard: "
-
Standard"
-
-
-
-
- LabelApiDocumentation: "
-
Api Documentation"
-
-
-
-
- LabelDeveloperResources: "
-
Developer Resources"
-
-
-
-
- LabelBrowseLibrary: "
-
Browse Library"
-
-
-
-
- LabelConfigureServer: "
-
Configure Emby"
-
-
-
-
- LabelOpenLibraryViewer: "
-
Open Library Viewer"
-
-
-
-
- LabelRestartServer: "
-
Restart Server"
-
-
-
-
- LabelShowLogWindow: "
-
Show Log Window"
-
-
-
-
- LabelPrevious: "
-
Previous"
-
-
- \wizardcomponents.html:54 -
- \wizardfinish.html:40 -
- \wizardlibrary.html:19 -
- \wizardlivetvguide.html:30 -
- \wizardlivetvtuner.html:31 -
- \wizardservice.html:17 -
- \wizardsettings.html:32 -
- \wizarduser.html:27 -
-
-
- LabelFinish: "
-
Finish"
-
-
- \wizardfinish.html:41 -
-
-
- LabelNext: "
-
Next"
-
-
- \wizardcomponents.html:55 -
- \wizardlibrary.html:20 -
- \wizardlivetvguide.html:31 -
- \wizardlivetvtuner.html:32 -
- \wizardservice.html:18 -
- \wizardsettings.html:33 -
- \wizardstart.html:25 -
- \wizarduser.html:28 -
-
-
- LabelYoureDone: "
-
You're Done!"
-
-
- \wizardfinish.html:7 -
-
-
- WelcomeToProject: "
-
Welcome to Emby!"
-
-
- \wizardstart.html:10 -
-
-
- ThisWizardWillGuideYou: "
-
This wizard will help guide you through the setup process. To begin, please select your preferred language."
-
-
- \wizardstart.html:16 -
-
-
- TellUsAboutYourself: "
-
Tell us about yourself"
-
-
- \wizarduser.html:8 -
-
-
- ButtonQuickStartGuide: "
-
Quick start guide"
-
-
- \wizardstart.html:12 -
-
-
- LabelYourFirstName: "
-
Your first name:"
-
-
- \wizarduser.html:14 -
-
-
- MoreUsersCanBeAddedLater: "
-
More users can be added later within the Dashboard."
-
-
- \wizarduser.html:15 -
-
-
- UserProfilesIntro: "
-
Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls."
-
-
- \wizarduser.html:11 -
-
-
- LabelWindowsService: "
-
Windows Service"
-
-
- \wizardservice.html:7 -
-
-
- AWindowsServiceHasBeenInstalled: "
-
A Windows Service has been installed."
-
-
- \wizardservice.html:10 -
-
-
- WindowsServiceIntro1: "
-
Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead."
-
-
- \wizardservice.html:12 -
-
-
- WindowsServiceIntro2: "
-
If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders."
-
-
- \wizardservice.html:14 -
-
-
- WizardCompleted: "
-
That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click <b>Finish</b> to view the <b>Server Dashboard</b>."
-
-
- \wizardfinish.html:10 -
-
-
- LabelConfigureSettings: "
-
Configure settings"
-
-
- \wizardsettings.html:8 -
-
-
- LabelEnableVideoImageExtraction: "
-
Enable video image extraction"
-
-
-
-
- VideoImageExtractionHelp: "
-
For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation."
-
-
-
-
- LabelEnableChapterImageExtractionForMovies: "
-
Extract chapter image extraction for Movies"
-
-
-
-
- LabelChapterImageExtractionForMoviesHelp: "
-
Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours."
-
-
-
-
- LabelEnableAutomaticPortMapping: "
-
Enable automatic port mapping"
-
-
-
-
- LabelEnableAutomaticPortMappingHelp: "
-
UPnP allows automated router configuration for easy remote access. This may not work with some router models."
-
-
-
-
- HeaderTermsOfService: "
-
Emby Terms of Service"
-
-
-
-
- HeaderDeveloperOptions: "
-
Developer Options"
-
-
- \dashboardgeneral.html:108 -
-
-
- OptionEnableWebClientResponseCache: "
-
Enable web response caching"
-
-
- \dashboardgeneral.html:112 -
-
-
- OptionDisableForDevelopmentHelp: "
-
Configure these as needed for web development purposes."
-
-
- \dashboardgeneral.html:119 -
-
-
- OptionEnableWebClientResourceMinification: "
-
Enable web resource minification"
-
-
- \dashboardgeneral.html:116 -
-
-
- LabelDashboardSourcePath: "
-
Web client source path:"
-
-
- \dashboardgeneral.html:124 -
-
-
- LabelDashboardSourcePathHelp: "
-
If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location."
-
-
- \dashboardgeneral.html:126 -
-
-
- ButtonConvertMedia: "
-
Convert media"
-
-
- \syncactivity.html:22 -
-
-
- ButtonOrganize: "
-
Organize"
-
-
- \autoorganizelog.html:8 -
- \scripts\autoorganizelog.js:293 -
- \scripts\autoorganizelog.js:294 -
- \scripts\autoorganizelog.js:296 -
-
-
- LinkedToEmbyConnect: "
-
Linked to Emby Connect"
-
-
-
-
- HeaderSupporterBenefits: "
-
Emby Premiere Benefits"
-
-
-
-
- HeaderAddUser: "
-
Add User"
-
-
-
-
- LabelAddConnectSupporterHelp: "
-
To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page."
-
-
-
-
- LabelPinCode: "
-
Pin code:"
-
-
-
-
- OptionHideWatchedContentFromLatestMedia: "
-
Hide watched content from latest media"
-
-
- \mypreferenceshome.html:114 -
-
-
- HeaderSync: "
-
Sync"
-
-
- \mysyncsettings.html:7 -
- \scripts\registrationservices.js:175 -
- \useredit.html:82 -
-
-
- ButtonOk: "
-
Ok"
-
-
- \components\directorybrowser\directorybrowser.js:147 -
- \components\fileorganizer\fileorganizer.template.html:45 -
- \components\medialibrarycreator\medialibrarycreator.template.html:30 -
- \components\metadataeditor\personeditor.template.html:33 -
- \dlnaprofile.html:372 -
- \dlnaprofile.html:453 -
- \dlnaprofile.html:504 -
- \dlnaprofile.html:542 -
- \dlnaprofile.html:590 -
- \dlnaprofile.html:630 -
- \dlnaprofile.html:661 -
- \dlnaprofile.html:706 -
- \nowplaying.html:113 -
- \scripts\ratingdialog.js:42 -
-
-
- ButtonCancel: "
-
Cancel"
-
-
- \components\tvproviders\schedulesdirect.template.html:68 -
- \components\tvproviders\xmltv.template.html:48 -
- \connectlogin.html:74 -
- \connectlogin.html:108 -
- \dlnaprofile.html:325 -
- \dlnaprofile.html:375 -
- \dlnaprofile.html:456 -
- \dlnaprofile.html:507 -
- \dlnaprofile.html:545 -
- \dlnaprofile.html:593 -
- \dlnaprofile.html:633 -
- \dlnaprofile.html:664 -
- \dlnaprofile.html:709 -
- \forgotpassword.html:23 -
- \forgotpasswordpin.html:22 -
- \livetvseriestimer.html:62 -
- \livetvtunerprovider-hdhomerun.html:35 -
- \livetvtunerprovider-m3u.html:19 -
- \livetvtunerprovider-satip.html:65 -
- \login.html:27 -
- \notificationsetting.html:64 -
- \scheduledtask.html:85 -
- \scripts\librarylist.js:349 -
- \scripts\mediacontroller.js:167 -
- \scripts\mediacontroller.js:436 -
- \scripts\ratingdialog.js:43 -
- \scripts\site.js:1025 -
- \scripts\userprofilespage.js:198 -
- \syncsettings.html:43 -
- \useredit.html:111 -
- \userlibraryaccess.html:57 -
- \usernew.html:45 -
- \userparentalcontrol.html:101 -
-
-
- ButtonExit: "
-
Exit"
-
-
-
-
- ButtonNew: "
-
New"
-
-
- \components\fileorganizer\fileorganizer.template.html:18 -
- \dlnaprofile.html:107 -
- \dlnaprofile.html:278 -
- \dlnaprofile.html:290 -
- \dlnaprofile.html:296 -
- \dlnaprofile.html:302 -
- \dlnaprofile.html:308 -
- \dlnaprofile.html:314 -
- \dlnaprofiles.html:14 -
- \serversecurity.html:8 -
-
-
- HeaderTaskTriggers: "
-
Task Triggers"
-
-
- \scheduledtask.html:11 -
-
-
- HeaderTV: "
-
TV"
-
-
- \librarysettings.html:113 -
-
-
- HeaderAudio: "
-
Audio"
-
-
- \librarysettings.html:39 -
-
-
- HeaderVideo: "
-
Video"
-
-
- \librarysettings.html:50 -
-
-
- HeaderPaths: "
-
Paths"
-
-
- \dashboard.html:92 -
-
-
- CategorySync: "
-
Sync"
-
-
-
-
- TabPlaylist: "
-
Playlist"
-
-
- \nowplaying.html:20 -
-
-
- HeaderEasyPinCode: "
-
Easy Pin Code"
-
-
- \myprofile.html:69 -
- \userpassword.html:42 -
-
-
- HeaderGrownupsOnly: "
-
Grown-ups Only!"
-
-
-
-
- DividerOr: "
-
-- or --"
-
-
-
-
- HeaderInstalledServices: "
-
Installed Services"
-
-
- \appservices.html:6 -
-
-
- HeaderAvailableServices: "
-
Available Services"
-
-
- \appservices.html:11 -
-
-
- - diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt deleted file mode 100644 index 39586022b..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - -]> - - - - - - - - <xsl:value-of select="StringUsages/@ReportTitle"/> - - - - -

- -

-
-

Strings

-
- - - - - - - - - - - - - - -
-
: "
-
"
-
:
-
-
- - -
-
\ No newline at end of file diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml deleted file mode 100644 index 9c65bddcd..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs deleted file mode 100644 index 1fd511e86..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs +++ /dev/null @@ -1,259 +0,0 @@ -using MediaBrowser.Tests.ConsistencyTests.TextIndexing; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; - -namespace MediaBrowser.Tests.ConsistencyTests -{ - /// - /// This class contains tests for reporting the usage of localization string tokens - /// in the dashboard-ui or similar. - /// - /// - /// Run one of the two tests using Visual Studio's "Test Explorer": - /// - /// - /// - /// - /// - /// - /// - /// On successful run, the bottom section of the test explorer will contain a link "Output". - /// This link will open the test results, displaying the trace and two attachment links. - /// One link will open the output folder, the other link will open the output xml file. - /// - /// - /// The output xml file contains a stylesheet link to render the results as html. - /// How that works depends on the default application configured for XML files: - /// - /// - /// Visual Studio - /// Will open in XML source view. To view the html result, click menu - /// 'XML' => 'Start XSLT without debugging' - /// Internet Explorer - /// XSL transform will be applied automatically. - /// Firefox - /// XSL transform will be applied automatically. - /// Chrome - /// Does not work. Chrome is unable/unwilling to apply xslt transforms from local files. - /// - /// - [TestClass] - public class StringUsageReporter - { - /// - /// Root path of the web application - /// - /// - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// - public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui"; - - /// - /// Path to the strings file, relative to . - /// - public const string StringsFile = @"strings\en-US.json"; - - /// - /// Path to the output folder - /// - /// - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// Important: When changing the output path, make sure that "StringCheck.xslt" is present - /// to make the XML transform work. - /// - public const string OutputPath = @"."; - - /// - /// List of file extension to search. - /// - public static string[] TargetExtensions = new[] { ".js", ".html" }; - - /// - /// List of paths to exclude from search. - /// - public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" }; - - private TestContext testContextInstance; - - /// - ///Gets or sets the test context which provides - ///information about and functionality for the current test run. - /// - public TestContext TestContext - { - get - { - return testContextInstance; - } - set - { - testContextInstance = value; - } - } - - //[TestMethod] - //public void ReportStringUsage() - //{ - // this.CheckDashboardStrings(false); - //} - - [TestMethod] - public void ReportUnusedStrings() - { - this.CheckDashboardStrings(true); - } - - private void CheckDashboardStrings(Boolean unusedOnly) - { - // Init Folders - var currentDir = System.IO.Directory.GetCurrentDirectory(); - Trace("CurrentDir: {0}", currentDir); - - var rootFolderInfo = ResolveFolder(currentDir, WebFolder); - Trace("Web Root: {0}", rootFolderInfo.FullName); - - var outputFolderInfo = ResolveFolder(currentDir, OutputPath); - Trace("Output Path: {0}", outputFolderInfo.FullName); - - // Load Strings - var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile); - - if (!File.Exists(stringsFileName)) - { - throw new Exception(string.Format("Strings file not found: {0}", stringsFileName)); - } - - int lineNumbers; - var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers); - - Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers); - - var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories); - - var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase))); - var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p))); - - var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList(); - - var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName); - - Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count); - - var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now)); - var settings = new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - WriteEndDocumentOnClose = true - }; - - Trace("Output file: {0}", outputFileName); - - using (XmlWriter writer = XmlWriter.Create(outputFileName, settings)) - { - writer.WriteStartDocument(true); - - // Write the Processing Instruction node. - string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\""; - writer.WriteProcessingInstruction("xml-stylesheet", xslText); - - writer.WriteStartElement("StringUsages"); - writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report"); - writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All"); - - foreach (var kvp in stringsDic) - { - var occurences = wordIndex.Find(kvp.Key); - - if (occurences == null || !unusedOnly) - { - ////Trace("{0}: {1}", kvp.Key, kvp.Value); - writer.WriteStartElement("Dictionary"); - writer.WriteAttributeString("Token", kvp.Key); - writer.WriteAttributeString("Text", kvp.Value); - - if (occurences != null && !unusedOnly) - { - foreach (var occurence in occurences) - { - writer.WriteStartElement("Occurence"); - writer.WriteAttributeString("FileName", occurence.FileName); - writer.WriteAttributeString("FullPath", occurence.FullPath); - writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString()); - writer.WriteEndElement(); - ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber); - } - } - - writer.WriteEndElement(); - } - } - } - - TestContext.AddResultFile(outputFileName); - TestContext.AddResultFile(outputFolderInfo.FullName); - } - - private SortedDictionary CreateStringsDictionary(FileInfo file, out int lineNumbers) - { - var dic = new SortedDictionary(); - lineNumbers = 0; - - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - lineNumbers++; - var words = reader - .ReadLine() - .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries); - - - if (words.Length == 2) - { - var token = words[0].Replace("\"", string.Empty).Trim(); - var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim(); - - if (dic.Keys.Contains(token)) - { - throw new Exception(string.Format("Double string entry found: {0}", token)); - } - - dic.Add(token, text); - } - } - } - - return dic; - } - - private DirectoryInfo ResolveFolder(string currentDir, string folderPath) - { - if (folderPath.IndexOf(@"\:") != 1) - { - folderPath = Path.Combine(currentDir, folderPath); - } - - var folderInfo = new DirectoryInfo(folderPath); - - if (!folderInfo.Exists) - { - throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName)); - } - - return folderInfo; - } - - - private void Trace(string message, params object[] parameters) - { - var formatted = string.Format(message, parameters); - System.Diagnostics.Trace.WriteLine(formatted); - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs deleted file mode 100644 index 4c46f4793..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class IndexBuilder - { - public const int MinumumWordLength = 4; - - public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); - - public static WordIndex BuildIndexFromFiles(IEnumerable wordFiles, string rootFolderPath) - { - var index = new WordIndex(); - - var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray(); - wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab - - foreach (var file in wordFiles) - { - var lineNumber = 1; - var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty); - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - var words = reader - .ReadLine() - .Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries); - ////.Select(f => f.Trim()); - - var wordIndex = 1; - foreach (var word in words) - { - if (word.Length >= MinumumWordLength) - { - index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++); - } - } - - lineNumber++; - } - } - } - - return index; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs deleted file mode 100644 index e0af08792..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordIndex : Dictionary - { - public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase) - { - } - - public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex) - { - WordOccurrences current; - if (!this.TryGetValue(word, out current)) - { - current = new WordOccurrences(); - this[word] = current; - } - - current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex); - } - - public WordOccurrences Find(string word) - { - WordOccurrences found; - if (this.TryGetValue(word, out found)) - { - return found; - } - - return null; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs deleted file mode 100644 index b30e58624..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public struct WordOccurrence - { - public readonly string FileName; // file containing the word. - public readonly string FullPath; // file containing the word. - public readonly int LineNumber; // line within the file. - public readonly int WordIndex; // index within the line. - - public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - FileName = fileName; - FullPath = fullPath; - LineNumber = lineNumber; - WordIndex = wordIndex; - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs deleted file mode 100644 index a6388ab54..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordOccurrences : List - { - public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex)); - } - - } -} diff --git a/MediaBrowser.Tests/M3uParserTest.cs b/MediaBrowser.Tests/M3uParserTest.cs deleted file mode 100644 index 583f5f5f0..000000000 --- a/MediaBrowser.Tests/M3uParserTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Emby.Server.Implementations.Cryptography; -using Emby.Server.Implementations.LiveTv.TunerHosts; -using MediaBrowser.Common.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests -{ - [TestClass] - public class M3uParserTest - { - [TestMethod] - public void TestFormat1() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0,84. VOX Schweiz\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("VOX Schweiz", result[0].Name); - Assert.AreEqual("84", result[0].Number); - } - [TestMethod] - public void TestFormat2() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var input = "#EXTINF:-1 tvg-id=\"\" tvg-name=\"ABC News 04\" tvg-logo=\"\" group-title=\"ABC Group\",ABC News 04"; - input += "\n"; - input += "http://mystream"; - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString(input, "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("ABC News 04", result[0].Name); - Assert.IsNull(result[0].Number); - } - - [TestMethod] - public void TestFormat3() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0, 3.2 - Movies!\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("Movies!", result[0].Name); - Assert.AreEqual("3.2", result[0].Number); - } - - [TestMethod] - public void TestFormat4() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0 tvg-id=\"abckabclosangeles.path.to\" tvg-logo=\"path.to / channel_logos / abckabclosangeles.png\", ABC KABC Los Angeles\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.IsNull(result[0].Number); - Assert.AreEqual("ABC KABC Los Angeles", result[0].Name); - } - - [TestMethod] - public void TestFormat5() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 channel-id=\"2101\" tvg-id=\"I69387.json.schedulesdirect.org\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - - [TestMethod] - public void TestFormat6() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 tvg-id=\"2101\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - } -} diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj deleted file mode 100644 index 6415d4211..000000000 --- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj +++ /dev/null @@ -1,139 +0,0 @@ - - - - Debug - AnyCPU - {E22BFD35-0FCD-4A85-978A-C22DCD73A081} - Library - Properties - MediaBrowser.Tests - MediaBrowser.Tests - v4.6.2 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\MediaBrowser.Tests.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\ThirdParty\emby\Emby.Server.MediaEncoding.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {e383961b-9356-4d5d-8233-9a1079d03055} - Emby.Server.Implementations - - - {9142eefa-7570-41e1-bfcc-468bb571af2f} - MediaBrowser.Common - - - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} - MediaBrowser.Controller - - - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} - MediaBrowser.Model - - - {442B5058-DCAF-4263-BB6A-F21E31120A1B} - MediaBrowser.Providers - - - {23499896-b135-4527-8574-c26e926ea99e} - MediaBrowser.XbmcMetadata - - - - - - - - - - - - Always - StringCheck.xslt - - - - - - - - - False - - - False - - - False - - - False - - - - - - - - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs deleted file mode 100644 index b69faab11..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Text; -using MediaBrowser.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class AssParserTests { - - [TestMethod] - public void TestParse() { - - var expectedSubs = - new SubtitleTrackInfo { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 72000000, - Text = - "Senator, we're "+ParserValues.NewLine+"making our final "+ParserValues.NewLine+"approach into Coruscant." - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 97100000, - EndPositionTicks = 133900000, - Text = - "Very good, Lieutenant." - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 150400000, - EndPositionTicks = 180400000, - Text = "It's "+ParserValues.NewLine+"a "+ParserValues.NewLine+"trap!" - } - } - }; - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length,result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - - [TestMethod] - public void TestParse2() - { - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data2.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - - using (var ms = new MemoryStream()) - { - var writer = new SrtWriter(); - writer.Write(result, ms, CancellationToken.None); - - ms.Position = 0; - var text = Encoding.UTF8.GetString(ms.ToArray()); - var b = text; - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs deleted file mode 100644 index aae96b382..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles -{ - - [TestClass] - public class SrtParserTests - { - - [TestMethod] - public void TestParse() - { - - var expectedSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!"+ParserValues.NewLine+"It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is RED and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a"+ParserValues.NewLine+"new line, as is"+ParserValues.NewLine+"this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested bold, italic, underline and strike-through HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new SrtParser(new NullLogger()); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length, result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass deleted file mode 100644 index 3114a844a..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass +++ /dev/null @@ -1,23 +0,0 @@ -[Script Info] -Title: Testing subtitles for the SSA Format - -[V4 Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding -Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0 -Style: Titre_episode,Akbar,140,15724527,65535,65535,986895,-1,0,1,1,0,3,30,30,30,0,0 -Style: Wolf main,Wolf_Rain,56,15724527,15724527,15724527,4144959,0,0,1,1,2,2,5,5,30,0,0 - - - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant. -Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant. -Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap! - - -[Pictures] -This section will be ignored - -[Fonts] -This section will be ignored \ No newline at end of file diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass deleted file mode 100644 index 98585f636..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass +++ /dev/null @@ -1,391 +0,0 @@ -[Script Info] -Title: English (US) -ScriptType: v4.00+ -WrapStyle: 0 -PlayResX: 640 -PlayResY: 360 - -[V4+ Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,0010,0010,0010,1 -Style: para-main,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0020,0020,0015,0 -Style: para-main-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-internal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-internal-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-overlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-narration,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000137,&H00000137,0,1,0,0,100,100,0,0,1,2,1,8,0020,0020,0015,0 -Style: para-internaloverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashback,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackinternal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0701,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackoverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H004D0701,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-title,arial,35,&H001F00C1,&H000000FF,&H00050058,&H00000137,1,0,0,0,100,100,0,0,1,1,0,7,0050,0020,0050,0 -Style: para-title-maxim,Times New Roman,25,&H00FFF3F3,&H000000FF,&H003B264A,&H00000137,0,0,0,0,100,100,0,0,1,1,0,4,0050,0020,0050,0 -Style: para-ep-title,Times New Roman,25,&H00F8FDFF,&H000000FF,&H005C5C5C,&H00273024,0,0,0,0,100,100,0,0,1,0,1,1,0056,0058,0060,0 -Style: para-next-ep,Trebuchet MS,22,&H009A8D94,&H000000FF,&H00000000,&H00273024,0,0,0,0,100,100,0,0,1,0,0,8,0000,0000,0135,0 -Style: tiny sign,Times New Roman,14,&H002C2F23,&H000000FF,&H00060600,&H00000000,1,0,0,0,100,100,0,0,1,2,0,8,0140,0010,0015,1 -Style: writing1,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0080,0010,0025,1 -Style: writing2,Verdana,12,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,3,0080,0090,0085,1 -Style: writing3,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0010,0130,0080,1 -Style: recept,Trebuchet MS,12,&H00AFB2AC,&H000000FF,&H004C4D49,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0010,0010,0020,1 -Style: food,Times New Roman,23,&H0056886C,&H000000FF,&H0083E5F9,&H00000000,1,0,0,0,100,100,0,0,1,4,0,7,0020,0010,0070,1 -Style: pad,Times New Roman,12,&H00445F6A,&H000000FF,&H007D6A4F,&H00000000,0,0,0,0,100,100,0,25,1,0,0,2,0040,0010,0105,1 -Style: chalk,Times New Roman,24,&H007B867F,&H000000FF,&H008EE3E9,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0050,0050,0055,1 -Style: fortune,Times New Roman,18,&H00153249,&H000000FF,&H00727FA4,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0060,0010,0030,1 -Style: fortune2,Times New Roman,24,&H003277AB,&H000000FF,&H00D0FFFF,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0080,0000,0020,1 - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text - -Dialogue: 0,0:00:06.89,0:00:10.62,para-main,M,0000,0000,0000,,I'm sorry to sour the mood, Shinichi, but... -Dialogue: 0,0:00:10.62,0:00:11.80,para-main,S,0000,0000,0000,,No way. -Dialogue: 0,0:00:11.80,0:00:12.49,para-main,S,0000,0000,0000,,You must be kidding. -Dialogue: 0,0:00:13.00,0:00:14.61,para-main,M,0000,0000,0000,,We need to start running right now. -Dialogue: 0,0:00:15.20,0:00:16.74,para-main,S,0000,0000,0000,,Are you sure it's him? -Dialogue: 0,0:00:17.25,0:00:20.06,para-main,M,0000,0000,0000,,These wavelengths are too \Npowerful to come from some lackey. -Dialogue: 0,0:00:20.06,0:00:22.81,para-main,M,0000,0000,0000,,And given the speed of his \Napproach, I'd say he's in a car. -Dialogue: 0,0:00:23.49,0:00:25.29,para-main,M,0000,0000,0000,,Take a right at that corner. -Dialogue: 0,0:00:25.76,0:00:26.72,para-main,S,0000,0000,0000,,Shit! -Dialogue: 0,0:00:26.72,0:00:28.43,para-main,S,0000,0000,0000,,Now I'm a thief. -Dialogue: 0,0:00:28.43,0:00:30.17,para-main,M,0000,0000,0000,,Is this the time to whine about it? -Dialogue: 0,0:00:31.10,0:00:34.25,para-main,S,0000,0000,0000,,When'd you learn to drive, anyway? -Dialogue: 0,0:00:34.70,0:00:37.80,para-main,M,0000,0000,0000,,I mastered Japanese in \Na single day, you know. -Dialogue: 0,0:00:39.72,0:00:41.46,para-main,S,0000,0000,0000,,Migi, I have a favor to ask. -Dialogue: 0,0:00:42.68,0:00:45.23,para-main,S,0000,0000,0000,,Please go somewhere with \Nas few people as possible. -Dialogue: 0,0:00:45.23,0:00:47.94,para-main,S,0000,0000,0000,,If we fight him in a city, \Nmany people will die. -Dialogue: 0,0:00:49.63,0:00:50.94,para-main,M,0000,0000,0000,,Very well. -Dialogue: 0,0:00:52.20,0:00:55.81,para-main,M,0000,0000,0000,,I've thought of something \Nthat's worth a gamble. -Dialogue: 0,0:01:35.53,0:01:42.66,para-title,,0000,0000,0000,,Parasyte -Dialogue: 0,0:01:37.74,0:01:42.66,para-title-maxim,,0000,0000,0000,,{\fad(2000,1)}The Maxim -Dialogue: 0,0:02:42.01,0:02:46.54,para-ep-title,Sign 0245,0000,0000,0000,,{\fad(350,500)\an3}Quiescence and Awakening -Dialogue: 0,0:02:48.95,0:02:49.69,para-main,S,0000,0000,0000,,Well? -Dialogue: 0,0:02:50.56,0:02:51.55,para-main,M,0000,0000,0000,,It didn't work. -Dialogue: 0,0:02:52.05,0:02:52.99,para-main,M,0000,0000,0000,,He's alive. -Dialogue: 0,0:02:53.89,0:02:55.55,para-main,M,0000,0000,0000,,He's tough. -Dialogue: 0,0:02:56.76,0:02:57.48,para-main,M,0000,0000,0000,,Let's go. -Dialogue: 0,0:02:58.00,0:02:58.82,para-main,S,0000,0000,0000,,Go where? -Dialogue: 0,0:03:00.01,0:03:00.87,para-main,M,0000,0000,0000,,Let's run. -Dialogue: 0,0:03:11.71,0:03:13.13,para-main,M,0000,0000,0000,,All right, stop. -Dialogue: 0,0:03:16.59,0:03:18.09,para-main,S,0000,0000,0000,,Why are we stopping? -Dialogue: 0,0:03:18.09,0:03:20.38,para-main,S,0000,0000,0000,,We can't afford to waste time here! -Dialogue: 0,0:03:20.38,0:03:21.47,para-main,M,0000,0000,0000,,Calm down. -Dialogue: 0,0:03:21.47,0:03:23.97,para-main,M,0000,0000,0000,,Let's strategize until he shows up. -Dialogue: 0,0:03:24.33,0:03:25.85,para-main,S,0000,0000,0000,,Strategize?! -Dialogue: 0,0:03:25.85,0:03:28.21,para-main,S,0000,0000,0000,,We might be minutes away \Nfrom being chopped up! -Dialogue: 0,0:03:28.21,0:03:29.24,para-main,M,0000,0000,0000,,Shinichi. -Dialogue: 0,0:03:29.24,0:03:30.69,para-main,M,0000,0000,0000,,I understand how you feel. -Dialogue: 0,0:03:30.69,0:03:32.48,para-main,M,0000,0000,0000,,Anyone would fear death. -Dialogue: 0,0:03:32.83,0:03:34.48,para-main,M,0000,0000,0000,,I'm afraid, as well. -Dialogue: 0,0:03:34.95,0:03:37.51,para-main,M,0000,0000,0000,,However, this is our moment of truth! -Dialogue: 0,0:03:39.54,0:03:43.25,para-main,M,0000,0000,0000,,You have a strength normal \Nhumans don't have. -Dialogue: 0,0:03:43.25,0:03:46.23,para-main,M,0000,0000,0000,,You can be calm, no matter \Nwhat the circumstance. -Dialogue: 0,0:03:46.85,0:03:48.89,para-main,M,0000,0000,0000,,Now, put your hand on your chest -Dialogue: 0,0:03:48.89,0:03:51.02,para-main,M,0000,0000,0000,,and breathe deeply like you always do. -Dialogue: 0,0:04:00.56,0:04:02.84,para-main,M,0000,0000,0000,,Good, well done. -Dialogue: 0,0:04:03.54,0:04:04.44,para-main,M,0000,0000,0000,,Listen. -Dialogue: 0,0:04:04.44,0:04:08.50,para-main,M,0000,0000,0000,,When it comes to ability, \NGotou surpasses us in every way. -Dialogue: 0,0:04:08.93,0:04:12.85,para-main,M,0000,0000,0000,,By simple calculations, our odds of \Nvictory might be zero percent. -Dialogue: 0,0:04:12.85,0:04:16.22,para-main,M,0000,0000,0000,,But that just means we should approach \Nthis from a different angle. -Dialogue: 0,0:04:16.79,0:04:20.18,para-main,M,0000,0000,0000,,If we can't win even by working together, -Dialogue: 0,0:04:20.85,0:04:23.36,para-main,M,0000,0000,0000,,maybe we should try {\i1}not{\i0} working together. -Dialogue: 0,0:04:23.63,0:04:24.24,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:04:25.01,0:04:28.12,para-main,M,0000,0000,0000,,In war, what matters is \Nopportunity, not numbers. -Dialogue: 0,0:04:28.12,0:04:29.27,para-main,S,0000,0000,0000,,Opportunity? -Dialogue: 0,0:04:29.86,0:04:33.16,para-main,M,0000,0000,0000,,In your pocket is a lighter I found in the car. -Dialogue: 0,0:04:42.79,0:04:44.01,para-main,Goto,0000,0000,0000,,They're above... -Dialogue: 0,0:04:44.31,0:04:48.01,para-main,Goto,0000,0000,0000,,They've spread out among \Nthe tree branches to hide. -Dialogue: 0,0:04:48.29,0:04:49.89,para-main,Goto,0000,0000,0000,,How unoriginal. -Dialogue: 0,0:04:50.90,0:04:52.68,para-flashbackinternal,M,0000,0000,0000,,This will be a race against time. -Dialogue: 0,0:04:53.27,0:04:57.40,para-flashbackinternal,M,0000,0000,0000,,To a parasite, the body is our lifeline \Nas well as our greatest weakness. -Dialogue: 0,0:04:58.18,0:05:01.39,para-flashbackinternal,M,0000,0000,0000,,My cells that have dispersed in your body -Dialogue: 0,0:05:01.39,0:05:05.21,para-flashbackinternal,M,0000,0000,0000,,have been completely integrated, \Nand are altered, -Dialogue: 0,0:05:05.21,0:05:06.91,para-flashbackinternal,M,0000,0000,0000,,so Gotou can't detect them. -Dialogue: 0,0:05:07.43,0:05:09.72,para-flashbackinternal,M,0000,0000,0000,,He will come straight for me -Dialogue: 0,0:05:09.72,0:05:11.66,para-flashbackinternal,M,0000,0000,0000,,without noticing your presence. -Dialogue: 0,0:05:12.17,0:05:15.50,para-flashbackinternal,M,0000,0000,0000,,If there is a protracted fight, \NI will shrivel up and die. -Dialogue: 0,0:05:15.50,0:05:17.67,para-flashbackinternal,M,0000,0000,0000,,This is an extremely reckless strategy. -Dialogue: 0,0:05:17.67,0:05:21.67,para-flashbackinternal,M,0000,0000,0000,,But that means even Gotou is \Nunlikely to anticipate our strategy. -Dialogue: 0,0:05:39.54,0:05:43.16,para-internal,Gotou,0000,0000,0000,,What, no counterattack? -Dialogue: 0,0:05:46.10,0:05:48.69,para-internal,Gotou,0000,0000,0000,,Where is the human boy? -Dialogue: 0,0:05:48.69,0:05:51.95,para-internal,Gotou,0000,0000,0000,,If only I can find and destroy the body, I'll win. -Dialogue: 0,0:05:53.56,0:05:54.58,para-flashbackinternal,M,0000,0000,0000,,His body -Dialogue: 0,0:05:55.03,0:05:58.58,para-flashbackinternal,M,0000,0000,0000,,is protected by semi-hardened parasite cells. -Dialogue: 0,0:05:59.12,0:06:02.59,para-flashbackinternal,M,0000,0000,0000,,It's unlikely that his entire body is armored, -Dialogue: 0,0:06:02.59,0:06:06.28,para-flashbackinternal,M,0000,0000,0000,,but there's no time to find where \Nthe chinks are in his armor. -Dialogue: 0,0:06:06.28,0:06:08.95,para-flashbackinternal,M,0000,0000,0000,,The part least likely to be armored -Dialogue: 0,0:06:08.95,0:06:12.47,para-flashbackinternal,M,0000,0000,0000,,and thus most suitable as a target... -Dialogue: 0,0:06:13.05,0:06:14.12,para-flashbackinternal,M,0000,0000,0000,,is his head. -Dialogue: 0,0:06:16.41,0:06:19.93,para-flashbackinternal,M,0000,0000,0000,,Unifying the multiple parasites \Nin his torso and limbs must -Dialogue: 0,0:06:19.93,0:06:22.95,para-flashbackinternal,M,0000,0000,0000,,require a tremendous amount of energy. -Dialogue: 0,0:06:22.95,0:06:25.96,para-flashbackinternal,M,0000,0000,0000,,Thus, the "head" has its hands \Nfull acting as the control tower. -Dialogue: 0,0:06:26.53,0:06:29.63,para-flashbackinternal,M,0000,0000,0000,,If we lop the head off, -Dialogue: 0,0:06:29.63,0:06:31.93,para-flashbackinternal,M,0000,0000,0000,,unity will be lost along with his armor, -Dialogue: 0,0:06:31.93,0:06:34.12,para-flashbackinternal,M,0000,0000,0000,,which should allow us to destroy his body. -Dialogue: 0,0:06:34.76,0:06:36.28,para-internal,S,0000,0000,0000,,Any time now, Migi! -Dialogue: 0,0:06:36.74,0:06:39.12,para-internal,S,0000,0000,0000,,If you don't hurry, you'll... -Dialogue: 0,0:06:39.64,0:06:41.33,para-internal,M,0000,0000,0000,,I will only have one chance! -Dialogue: 0,0:06:41.33,0:06:45.88,para-internal,M,0000,0000,0000,,If I am to decapitate Gotou when his \Npower and speed far surpasses my own... -Dialogue: 0,0:06:47.02,0:06:48.51,para-internal,M,0000,0000,0000,,What is this? -Dialogue: 0,0:06:48.51,0:06:50.73,para-internal,M,0000,0000,0000,,My consciousness is already fading... -Dialogue: 0,0:06:51.25,0:06:52.74,para-internal,M,0000,0000,0000,,I must hurry! -Dialogue: 0,0:06:52.74,0:06:54.89,para-internal,M,0000,0000,0000,,But the angle of attack is still poor. -Dialogue: 0,0:06:55.51,0:06:57.09,para-main,Gotou,0000,0000,0000,,Hey! Listen up! -Dialogue: 0,0:06:57.09,0:06:59.12,para-main,Gotou,0000,0000,0000,,Are you that scared of me?! -Dialogue: 0,0:06:59.12,0:07:03.19,para-main,Gotou,0000,0000,0000,,Spreading out in all directions \Nisn't much of a camouflage! -Dialogue: 0,0:07:03.51,0:07:06.69,para-main,Gotou,0000,0000,0000,,Use your brains to fight, not run! -Dialogue: 0,0:07:06.69,0:07:08.11,para-main,M,0000,0000,0000,,Now! Do it! -Dialogue: 0,0:07:08.62,0:07:09.49,para-internal,S,0000,0000,0000,,Was that my voice? -Dialogue: 0,0:07:09.88,0:07:11.24,para-main,G,0000,0000,0000,,There! -Dialogue: 0,0:07:17.41,0:07:20.29,para-flashbackinternal,M,0000,0000,0000,,The surface cells will instinctively disengage -Dialogue: 0,0:07:20.29,0:07:22.58,para-flashbackinternal,M,0000,0000,0000,,from Gotou's command upon exposure to fire, -Dialogue: 0,0:07:22.92,0:07:23.72,para-flashbackinternal,M,0000,0000,0000,,and as a result... -Dialogue: 0,0:07:28.94,0:07:29.67,para-internal,M,0000,0000,0000,,Damn! -Dialogue: 0,0:07:29.67,0:07:30.38,para-internal,M,0000,0000,0000,,Not deep enough! -Dialogue: 0,0:07:34.70,0:07:35.55,para-main,M,0000,0000,0000,,Did we fail? -Dialogue: 0,0:07:35.55,0:07:36.84,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:07:36.84,0:07:38.08,para-main,M,0000,0000,0000,,Stay back, Shinichi! -Dialogue: 0,0:07:39.79,0:07:40.77,para-main,M,0000,0000,0000,,We failed! -Dialogue: 0,0:07:41.31,0:07:43.43,para-main,Gotou,0000,0000,0000,,Well, this is a surprise. -Dialogue: 0,0:07:43.43,0:07:44.31,para-main,Gotou,0000,0000,0000,,Well done. -Dialogue: 0,0:07:44.52,0:07:45.52,para-main,M,0000,0000,0000,,Run! Now! -Dialogue: 0,0:07:45.52,0:07:46.33,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:07:46.33,0:07:47.36,para-main,M,0000,0000,0000,,Don't come any closer! -Dialogue: 0,0:07:47.61,0:07:48.98,para-main,M,0000,0000,0000,,We don't both need to die! -Dialogue: 0,0:07:52.43,0:07:54.11,para-main,S,0000,0000,0000,,But, Migi... -Dialogue: 0,0:07:59.77,0:08:00.83,para-main,M,0000,0000,0000,,What are you doing?! -Dialogue: 0,0:08:00.83,0:08:01.62,para-main,M,0000,0000,0000,,Hurry up and go! -Dialogue: 0,0:08:10.76,0:08:12.38,para-internal,M,0000,0000,0000,,Goodbye, Shinichi. -Dialogue: 0,0:08:13.06,0:08:15.48,para-internal,M,0000,0000,0000,,This is farewell, Shinichi. -Dialogue: 0,0:08:16.52,0:08:21.88,para-internal,M,0000,0000,0000,,I'm glad I didn't take over \Nyour brain when we first met. -Dialogue: 0,0:08:22.86,0:08:27.24,para-internal,M,0000,0000,0000,,Thanks to that, we made many \Ngood memories as friends... -Dialogue: 0,0:08:33.43,0:08:35.48,para-internal,M,0000,0000,0000,,I'm fading... -Dialogue: 0,0:08:35.91,0:08:37.32,para-internal,M,0000,0000,0000,,I feel oddly sleepy, -Dialogue: 0,0:08:38.47,0:08:42.85,para-internal,M,0000,0000,0000,,yet it's all eclipsed by the \Nfeeling that I'm so alone. -Dialogue: 0,0:08:45.37,0:08:46.44,para-internal,M,0000,0000,0000,,So this... -Dialogue: 0,0:08:47.45,0:08:48.37,para-internal,M,0000,0000,0000,,is death... -Dialogue: 0,0:09:48.98,0:09:49.91,para-main,Mitsu,0000,0000,0000,,Who's there?! -Dialogue: 0,0:09:53.90,0:09:55.91,para-main,S,0000,0000,0000,,Oh, sorry. -Dialogue: 0,0:09:55.91,0:09:57.35,para-main,Mitsu,0000,0000,0000,,A... A burglar?! -Dialogue: 0,0:09:57.35,0:09:58.41,para-main,S,0000,0000,0000,,No! -Dialogue: 0,0:09:59.03,0:10:00.66,para-main,S,0000,0000,0000,,I'm not... But... -Dialogue: 0,0:10:00.66,0:10:02.82,para-main,S,0000,0000,0000,,Sure. You can call me that. -Dialogue: 0,0:10:02.82,0:10:03.70,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:10:04.52,0:10:07.71,para-main,S,0000,0000,0000,,I did try to drink some water \Nwithout permission, after all. -Dialogue: 0,0:10:07.71,0:10:09.55,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:10:09.55,0:10:11.97,para-main,Mitsu,0000,0000,0000,,Water isn't free, either. -Dialogue: 0,0:10:12.55,0:10:14.19,para-main,S,0000,0000,0000,,S-Sorry... -Dialogue: 0,0:10:14.82,0:10:16.39,para-main,S,0000,0000,0000,,Well, uh... -Dialogue: 0,0:10:16.87,0:10:18.19,para-main,S,0000,0000,0000,,I should go. -Dialogue: 0,0:10:18.19,0:10:19.75,para-main,S,0000,0000,0000,,I'm sorry for the trouble. -Dialogue: 0,0:10:23.35,0:10:24.99,para-main,Mitsu,0000,0000,0000,,Hang on a second. -Dialogue: 0,0:10:24.99,0:10:25.87,para-main,S,0000,0000,0000,,Yes? -Dialogue: 0,0:10:25.87,0:10:27.39,para-main,Mitsu,0000,0000,0000,,You're hurt. -Dialogue: 0,0:10:27.67,0:10:28.31,para-main,S,0000,0000,0000,,Oh... -Dialogue: 0,0:10:28.88,0:10:30.54,para-main,S,0000,0000,0000,,Well, no, uh... -Dialogue: 0,0:10:30.54,0:10:33.07,para-main,Mitsu,0000,0000,0000,,No, your head. -Dialogue: 0,0:10:33.07,0:10:36.03,para-main,Mitsu,0000,0000,0000,,You lost your right arm a long \Ntime ago, from the looks of it. -Dialogue: 0,0:10:37.39,0:10:38.44,para-main,S,0000,0000,0000,,I'm fine. -Dialogue: 0,0:10:38.93,0:10:40.77,para-main,S,0000,0000,0000,,I think the bleeding's already stopped. -Dialogue: 0,0:10:40.77,0:10:43.40,para-main,Mitsu,0000,0000,0000,,Just come in and let me take a look. -Dialogue: 0,0:10:43.81,0:10:44.41,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:10:44.41,0:10:45.74,para-main,Mitsu,0000,0000,0000,,Hurry up! -Dialogue: 0,0:10:45.74,0:10:48.69,para-main,Mitsu,0000,0000,0000,,A burglar wouldn't be this polite. -Dialogue: 0,0:10:48.69,0:10:51.29,para-main,Mitsu,0000,0000,0000,,Besides, you look like you've been crying. -Dialogue: 0,0:10:54.13,0:10:57.55,para-main,Mitsu,0000,0000,0000,,I worked in retail for a long time. -Dialogue: 0,0:10:57.55,0:11:01.89,para-main,Mitsu,0000,0000,0000,,I can tell a lot about a person from just one look. -Dialogue: 0,0:11:03.49,0:11:09.26,para-main,Mitsu,0000,0000,0000,,This injury wasn't from a fair fight, \Nwas it? You were bullied. -Dialogue: 0,0:11:09.26,0:11:10.39,para-main,S,0000,0000,0000,,Uh... -Dialogue: 0,0:11:11.56,0:11:14.15,para-main,Mitsu,0000,0000,0000,,The cut's pretty deep. -Dialogue: 0,0:11:14.70,0:11:18.14,para-main,Mitsu,0000,0000,0000,,Some people in this world do terrible things. -Dialogue: 0,0:11:30.20,0:11:33.22,para-main,S,0000,0000,0000,,I didn't expect so much kindness \Nfrom a complete stranger. -Dialogue: 0,0:11:34.82,0:11:36.92,para-main,S,0000,0000,0000,,Thank you for everything. -Dialogue: 0,0:11:37.35,0:11:39.76,para-main,Mitsu,0000,0000,0000,,Stay the night. -Dialogue: 0,0:11:39.76,0:11:40.48,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:11:40.48,0:11:42.34,para-main,S,0000,0000,0000,,I couldn't possibly... -Dialogue: 0,0:11:43.32,0:11:45.73,para-main,Mitsu,0000,0000,0000,,Where do you expect to go this late at night? -Dialogue: 0,0:11:45.73,0:11:48.05,para-main,Mitsu,0000,0000,0000,,There are no hotels around here! -Dialogue: 0,0:11:48.79,0:11:52.32,para-main,S,0000,0000,0000,,Um, do you live here by yourself, Granny? -Dialogue: 0,0:11:52.74,0:11:54.89,para-main,Mitsu,0000,0000,0000,,I don't have any grandchildren your age. -Dialogue: 0,0:11:55.41,0:11:56.91,para-main,S,0000,0000,0000,,Erm, Auntie? -Dialogue: 0,0:11:56.91,0:11:58.40,para-main,Mitsu,0000,0000,0000,,I don't have any nephews, either. -Dialogue: 0,0:11:59.38,0:12:01.40,para-main,Mitsu,0000,0000,0000,,My name is Mitsuyo. -Dialogue: 0,0:12:02.65,0:12:03.80,para-main,S,0000,0000,0000,,I'm sorry. -Dialogue: 0,0:12:04.31,0:12:06.91,para-main,S,0000,0000,0000,,I'm Izumi Shinichi. -Dialogue: 0,0:12:07.72,0:12:09.24,para-main,Mitsu,0000,0000,0000,,Shinichi, eh? -Dialogue: 0,0:12:09.86,0:12:11.22,para-main,Mitsu,0000,0000,0000,,Shin-chan, then. -Dialogue: 0,0:12:13.27,0:12:16.75,para-main,Mitsu,0000,0000,0000,,Sorry for making you help with the shopping. -Dialogue: 0,0:12:16.75,0:12:18.20,para-main,S,0000,0000,0000,,Oh, no problem. -Dialogue: 0,0:12:18.75,0:12:20.10,para-main,S,0000,0000,0000,,I can at least do that much. -Dialogue: 0,0:12:22.76,0:12:26.09,para-main,Mitsu,0000,0000,0000,,Let's just say you're my nephew. -Dialogue: 0,0:12:26.09,0:12:26.84,para-main,S,0000,0000,0000,,Nephew? -Dialogue: 0,0:12:26.84,0:12:29.39,para-main,Mitsu,0000,0000,0000,,Strange things have been \Nhappening around here lately. -Dialogue: 0,0:12:29.39,0:12:31.48,para-main,Mitsu,0000,0000,0000,,They're suspicious of outsiders. -Dialogue: 0,0:12:31.87,0:12:33.17,para-main,S,0000,0000,0000,,Strange things? -Dialogue: 0,0:12:36.59,0:12:38.12,para-main,Mitsu,0000,0000,0000,,This is what I was talking about. -Dialogue: 0,0:12:38.98,0:12:43.54,para-main,Mitsu,0000,0000,0000,,Someone keeps dumping truckloads \Nof garbage without permission. -Dialogue: 0,0:12:43.54,0:12:48.49,para-main,Mitsu,0000,0000,0000,,One time, it caught fire and nearly \Nset the entire mountain ablaze. -Dialogue: 0,0:12:48.85,0:12:52.74,para-main,Mitsu,0000,0000,0000,,I know they say big cities \Nare running out of landfills, -Dialogue: 0,0:12:52.74,0:12:55.11,para-main,Mitsu,0000,0000,0000,,but this is a bit much, don't you think? -Dialogue: 0,0:12:55.11,0:12:55.93,para-main,S,0000,0000,0000,,Yes... -Dialogue: 0,0:12:56.36,0:12:58.27,para-main,Mitsu,0000,0000,0000,,Those of us who live around here -Dialogue: 0,0:12:58.27,0:13:00.84,para-main,Mitsu,0000,0000,0000,,have been keeping watch day and night, -Dialogue: 0,0:13:01.16,0:13:03.50,para-main,Mitsu,0000,0000,0000,,but they're no-shows when we do keep watch -Dialogue: 0,0:13:03.50,0:13:06.49,para-main,Mitsu,0000,0000,0000,,and come the one day we sleep. -Dialogue: 0,0:13:06.49,0:13:09.38,para-internal,S,0000,0000,0000,,Mitsuyo-san had a sharp tongue, -Dialogue: 0,0:13:09.38,0:13:10.86,para-internal,S,0000,0000,0000,,but she was kindhearted. -Dialogue: 0,0:13:11.78,0:13:14.98,para-internal,S,0000,0000,0000,,Whenever I tried to thank her and leave, -Dialogue: 0,0:13:14.98,0:13:18.73,para-internal,S,0000,0000,0000,,she'd stop me with a machine gun \Nbarrage of conversation. -Dialogue: 0,0:13:19.43,0:13:20.86,para-internal,S,0000,0000,0000,,With Migi gone, -Dialogue: 0,0:13:20.86,0:13:23.55,para-internal,S,0000,0000,0000,,I had no idea what to do next. -Dialogue: 0,0:13:23.55,0:13:27.06,para-internal,S,0000,0000,0000,,I ended up staying several days. -Dialogue: 0,0:13:28.68,0:13:31.83,para-internal,S,0000,0000,0000,,But I can't impose on her forever. -Dialogue: 0,0:13:32.36,0:13:36.87,para-internal,S,0000,0000,0000,,I should go home tomorrow \Nand tell Dad everything. -Dialogue: 0,0:13:37.52,0:13:39.30,para-internal,S,0000,0000,0000,,About why I lost my right arm... -Dialogue: 0,0:13:39.93,0:13:42.39,para-internal,S,0000,0000,0000,,About how I had a friend named Migi... -Dialogue: 0,0:13:43.11,0:13:45.54,para-internal,S,0000,0000,0000,,About the day Migi first showed up in my life. -Dialogue: 0,0:13:46.48,0:13:48.39,para-internal,S,0000,0000,0000,,About the days we spent together. -Dialogue: 0,0:13:49.08,0:13:52.73,para-internal,S,0000,0000,0000,,And about how great a guy he was. -Dialogue: 0,0:13:53.94,0:13:55.77,para-internal,S,0000,0000,0000,,To save my life, he... -Dialogue: 0,0:13:55.77,0:13:58.14,para-internal,S,0000,0000,0000,,His intelligence, his courage... -Dialogue: 0,0:13:58.87,0:14:01.43,para-internal,S,0000,0000,0000,,I can't even hope to come \Nclose to him in any way. -Dialogue: 0,0:14:03.01,0:14:06.65,para-internal,S,0000,0000,0000,,He is a true hero! -Dialogue: 0,0:14:11.44,0:14:12.87,para-internal,S,0000,0000,0000,,Where am I? -Dialogue: 0,0:14:12.87,0:14:15.31,para-internal,S,0000,0000,0000,,I think I've been here before. -Dialogue: 0,0:14:16.02,0:14:17.22,para-internal,S,0000,0000,0000,,What's that? -Dialogue: 0,0:14:17.67,0:14:19.70,para-internal,S,0000,0000,0000,,Uh, who're you? -Dialogue: 0,0:14:19.70,0:14:20.63,para-internal,M,0000,0000,0000,,What is it? -Dialogue: 0,0:14:20.63,0:14:22.21,para-internal,M,0000,0000,0000,,Are you looking for something? -Dialogue: 0,0:14:22.21,0:14:24.08,para-internal,S,0000,0000,0000,,Looking? -Dialogue: 0,0:14:24.08,0:14:26.42,para-internal,S,0000,0000,0000,,Yeah, I'm looking for a friend. -Dialogue: 0,0:14:26.42,0:14:27.75,para-internal,M,0000,0000,0000,,A friend? -Dialogue: 0,0:14:28.17,0:14:30.27,para-internal,M,0000,0000,0000,,What does this friend look like? -Dialogue: 0,0:14:30.27,0:14:31.09,para-internal,S,0000,0000,0000,,Look like? -Dialogue: 0,0:14:31.72,0:14:34.12,para-internal,S,0000,0000,0000,,I don't really remember. -Dialogue: 0,0:14:34.12,0:14:35.43,para-internal,M,0000,0000,0000,,Then I can't help you. -Dialogue: 0,0:14:35.43,0:14:37.58,para-internal,S,0000,0000,0000,,Hey, wait. -Dialogue: 0,0:14:37.58,0:14:39.22,para-internal,S,0000,0000,0000,,I think he looked like you... -Dialogue: 0,0:14:39.76,0:14:41.81,para-internal,S,0000,0000,0000,,Right! I remember now! -Dialogue: 0,0:14:41.81,0:14:42.95,para-internal,S,0000,0000,0000,,He... -Dialogue: 0,0:14:42.95,0:14:44.86,para-internal,S,0000,0000,0000,,He died. -Dialogue: 0,0:14:44.86,0:14:46.83,para-internal,M,0000,0000,0000,,What? He's dead? -Dialogue: 0,0:14:46.83,0:14:47.69,para-internal,S,0000,0000,0000,,Yeah... -Dialogue: 0,0:14:48.28,0:14:51.43,para-internal,M,0000,0000,0000,,No, he's alive. -Dialogue: 0,0:14:51.43,0:14:52.07,para-internal,S,0000,0000,0000,,What?! -Dialogue: 0,0:14:52.16,0:14:53.97,para-internal,M,0000,0000,0000,,I can tell. -Dialogue: 0,0:14:53.97,0:14:56.60,para-internal,M,0000,0000,0000,,I actually know his name, too. -Dialogue: 0,0:14:56.60,0:14:58.55,para-internal,S,0000,0000,0000,,His... name? -Dialogue: 0,0:14:58.55,0:15:00.30,para-internal,S,0000,0000,0000,,His name... -Dialogue: 0,0:15:01.27,0:15:02.26,para-main,S,0000,0000,0000,,Migi?! -Dialogue: 0,0:15:07.49,0:15:08.26,para-main,S,0000,0000,0000,,Huh?! -Dialogue: 0,0:15:09.76,0:15:10.46,para-main,S,0000,0000,0000,,M... -Dialogue: 0,0:15:10.88,0:15:11.64,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:12.60,0:15:14.76,para-internal,S,0000,0000,0000,,Some of his cells are still here! -Dialogue: 0,0:15:15.68,0:15:17.37,para-main,S,0000,0000,0000,,Hey! It's me! -Dialogue: 0,0:15:17.37,0:15:18.50,para-main,S,0000,0000,0000,,Do you recognize me?! -Dialogue: 0,0:15:18.50,0:15:20.85,para-main,Mitsu,0000,0000,0000,,Keep it down. -Dialogue: 0,0:15:21.30,0:15:23.62,para-main,Mitsu,0000,0000,0000,,Go back to sleep. -Dialogue: 0,0:15:28.12,0:15:29.25,para-internal,S,0000,0000,0000,,It won't work. -Dialogue: 0,0:15:29.74,0:15:31.75,para-internal,S,0000,0000,0000,,Even if he can make a small eye, -Dialogue: 0,0:15:31.75,0:15:34.21,para-internal,S,0000,0000,0000,,it's not enough to be capable \Nof thought or speech. -Dialogue: 0,0:15:36.08,0:15:36.93,para-internal,S,0000,0000,0000,,Migi... -Dialogue: 0,0:15:38.42,0:15:39.24,para-internal,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:54.23,0:15:55.45,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:15:55.45,0:15:57.42,para-main,Mitsu,0000,0000,0000,,I guess I don't have a choice. -Dialogue: 0,0:15:57.42,0:16:00.75,para-main,Mitsu,0000,0000,0000,,I can't keep you here forever. -Dialogue: 0,0:16:00.75,0:16:03.48,para-main,S,0000,0000,0000,,I don't know how I can ever repay you. -Dialogue: 0,0:16:08.17,0:16:09.58,para-main,Mitsu,0000,0000,0000,,What's the matter? -Dialogue: 0,0:16:09.58,0:16:11.49,para-main,Mitsu,0000,0000,0000,,Why're you here so early in the morning? -Dialogue: 0,0:16:11.49,0:16:13.00,para-main,Taoka,0000,0000,0000,,Hey, you! -Dialogue: 0,0:16:13.00,0:16:15.28,para-main,Taoka,0000,0000,0000,,Are you really Mitsuyo-san's nephew? -Dialogue: 0,0:16:15.89,0:16:18.16,para-main,Mitsu,0000,0000,0000,,What does it matter? -Dialogue: 0,0:16:18.16,0:16:20.54,para-main,Mitsu,0000,0000,0000,,He's about to leave. -Dialogue: 0,0:16:20.54,0:16:22.05,para-main,Taoka,0000,0000,0000,,Not so fast. -Dialogue: 0,0:16:22.05,0:16:24.79,para-main,Taoka,0000,0000,0000,,There are way too many strange \Nthings happening lately. -Dialogue: 0,0:16:24.79,0:16:26.70,para-main,Taoka,0000,0000,0000,,The illegal trash dumping, -Dialogue: 0,0:16:26.70,0:16:29.05,para-main,Taoka,0000,0000,0000,,the car crash between two \Ndriver-less vehicles, -Dialogue: 0,0:16:30.29,0:16:32.30,para-main,Taoka,0000,0000,0000,,and now, murder. -Dialogue: 0,0:16:32.65,0:16:33.88,para-main,Mitsu,0000,0000,0000,,What?! -Dialogue: 0,0:16:34.09,0:16:37.24,para-main,Taoka,0000,0000,0000,,Isn't it always outsiders who commit crimes? -Dialogue: 0,0:16:37.24,0:16:38.85,para-main,Taoka,0000,0000,0000,,Outsiders like him? -Dialogue: 0,0:16:41.62,0:16:45.50,para-main,Nakano,0000,0000,0000,,I told you, that thing was \Nbeyond being an outsider. -Dialogue: 0,0:16:45.50,0:16:47.74,para-main,Mitsu,0000,0000,0000,,He's been with me the entire time— -Dialogue: 0,0:16:47.15,0:16:49.19,para-overlap,Nakano,0000,0000,0000,,It wasn't human! -Dialogue: 0,0:16:49.59,0:16:51.40,para-main,Nakano,0000,0000,0000,,I wasn't just seeing things! -Dialogue: 0,0:16:51.40,0:16:53.24,para-main,Nakano,0000,0000,0000,,It was at least three meters tall! -Dialogue: 0,0:16:53.78,0:16:54.45,para-main,Nakano,0000,0000,0000,,And its legs! -Dialogue: 0,0:16:54.45,0:16:56.70,para-main,Nakano,0000,0000,0000,,Yeah, it had four front legs alone! -Dialogue: 0,0:16:56.98,0:16:59.49,para-main,Nakano,0000,0000,0000,,It had more than three eyes, too! -Dialogue: 0,0:17:00.45,0:17:01.79,para-internal,S,0000,0000,0000,,It's Gotou... -Dialogue: 0,0:17:01.79,0:17:03.20,para-internal,S,0000,0000,0000,,It has to be! -Dialogue: 0,0:17:03.20,0:17:06.23,para-main,Nakano,0000,0000,0000,,Yeah, laugh at me all you want. -Dialogue: 0,0:17:06.23,0:17:09.75,para-main,Nakano,0000,0000,0000,,But see this blood? It's all Kitayama's! -Dialogue: 0,0:17:10.19,0:17:14.85,para-main,Nakano,0000,0000,0000,,Kitayama was killed and eaten \Nby a monster right close by! -Dialogue: 0,0:17:19.80,0:17:22.29,para-main,Det,0000,0000,0000,,So it happened around here? -Dialogue: 0,0:17:22.29,0:17:24.43,para-main,Nakano,0000,0000,0000,,Th-That's right. -Dialogue: 0,0:17:29.90,0:17:31.64,para-main,Naitou,0000,0000,0000,,This is horrible. -Dialogue: 0,0:17:31.64,0:17:34.65,para-main,Cop,0000,0000,0000,,Wait, something similar's happened before... -Dialogue: 0,0:17:35.46,0:17:37.08,para-main,Det,0000,0000,0000,,The Mincemeat Murders? -Dialogue: 0,0:17:38.54,0:17:40.10,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:17:40.10,0:17:42.04,para-main,Mitsu,0000,0000,0000,,Okay, got it. -Dialogue: 0,0:17:46.32,0:17:49.76,para-main,Mitsu,0000,0000,0000,,A bunch of hunters are going \Nto get together tomorrow, -Dialogue: 0,0:17:49.76,0:17:53.05,para-main,Mitsu,0000,0000,0000,,so they should be able to take \Ndown this "monster" then. -Dialogue: 0,0:17:55.04,0:17:58.12,para-main,S,0000,0000,0000,,Hunting rifles won't be \Nenough to take him down. -Dialogue: 0,0:17:58.12,0:17:58.90,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:17:58.90,0:18:00.05,para-main,Mitsu,0000,0000,0000,,"Him"? -Dialogue: 0,0:18:00.73,0:18:03.31,para-main,Mitsu,0000,0000,0000,,You know the monster? -Dialogue: 0,0:18:04.05,0:18:06.09,para-main,S,0000,0000,0000,,He's here because he's after me. -Dialogue: 0,0:18:06.57,0:18:08.01,para-main,S,0000,0000,0000,,To kill me. -Dialogue: 0,0:18:08.42,0:18:09.39,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:18:09.39,0:18:10.94,para-main,Mitsu,0000,0000,0000,,Stop talking nonsense. -Dialogue: 0,0:18:11.26,0:18:13.24,para-main,S,0000,0000,0000,,This is my fault! -Dialogue: 0,0:18:13.24,0:18:14.73,para-main,S,0000,0000,0000,,I brought him here! -Dialogue: 0,0:18:14.73,0:18:16.36,para-main,S,0000,0000,0000,,And someone was killed! -Dialogue: 0,0:18:17.15,0:18:20.18,para-main,S,0000,0000,0000,,Many more will die tomorrow \Nif I don't do something! -Dialogue: 0,0:18:21.02,0:18:22.41,para-main,Mitsu,0000,0000,0000,,Shin-chan... -Dialogue: 0,0:18:23.25,0:18:24.87,para-main,S,0000,0000,0000,,I just wanted to keep myself alive. -Dialogue: 0,0:18:25.38,0:18:26.85,para-main,S,0000,0000,0000,,Whatever it took. -Dialogue: 0,0:18:27.54,0:18:30.15,para-main,S,0000,0000,0000,,Friends have died for me, too. -Dialogue: 0,0:18:30.71,0:18:35.20,para-main,S,0000,0000,0000,,But I can't just keep running away on my own. -Dialogue: 0,0:18:36.07,0:18:37.43,para-main,Mitsu,0000,0000,0000,,Why not? -Dialogue: 0,0:18:41.97,0:18:43.90,para-main,Mitsu,0000,0000,0000,,Why not live? -Dialogue: 0,0:18:43.90,0:18:45.81,para-main,Mitsu,0000,0000,0000,,Why not run? -Dialogue: 0,0:18:45.81,0:18:48.98,para-main,Mitsu,0000,0000,0000,,Run if it's only to save your own life. -Dialogue: 0,0:18:49.27,0:18:51.68,para-main,Mitsu,0000,0000,0000,,It's nothing to be ashamed of. -Dialogue: 0,0:18:52.39,0:18:57.15,para-main,S,0000,0000,0000,,Mitsuyo-san, I haven't done \Neverything I can just yet! -Dialogue: 0,0:18:57.80,0:19:00.79,para-main,S,0000,0000,0000,,I have to make use of my life \Nbefore a group of people -Dialogue: 0,0:19:00.79,0:19:03.28,para-main,S,0000,0000,0000,,come face-to-face with \Nthat monster tomorrow! -Dialogue: 0,0:19:04.33,0:19:05.22,para-main,Mitsu,0000,0000,0000,,You idiot! -Dialogue: 0,0:19:05.22,0:19:06.45,para-main,Mitsu,0000,0000,0000,,Cut the bullshit! -Dialogue: 0,0:19:06.45,0:19:08.99,para-main,Mitsu,0000,0000,0000,,Make use of your life? -Dialogue: 0,0:19:08.99,0:19:10.96,para-main,Mitsu,0000,0000,0000,,How dare you speak so \Nlightly of your own life?! -Dialogue: 0,0:19:11.33,0:19:13.14,para-main,Mitsu,0000,0000,0000,,Who do you think you are?! -Dialogue: 0,0:19:13.14,0:19:14.88,para-main,Mitsu,0000,0000,0000,,Use your life? -Dialogue: 0,0:19:14.88,0:19:16.40,para-main,Mitsu,0000,0000,0000,,Don't make me laugh! -Dialogue: 0,0:19:16.40,0:19:19.47,para-main,Mitsu,0000,0000,0000,,What does a snot-nosed brat \Nlike you expect to do?! -Dialogue: 0,0:19:24.99,0:19:27.65,para-main,Mitsu,0000,0000,0000,,Look, I don't know your story, -Dialogue: 0,0:19:27.65,0:19:31.42,para-main,Mitsu,0000,0000,0000,,but you should just let adults \Nhandle this sort of thing. -Dialogue: 0,0:19:40.56,0:19:42.61,para-main,Mitsu,0000,0000,0000,,You're still leaving? -Dialogue: 0,0:19:43.78,0:19:48.70,para-main,Mitsu,0000,0000,0000,,Don't you have someone \Nin your life you care about? -Dialogue: 0,0:19:48.70,0:19:50.98,para-main,Mitsu,0000,0000,0000,,Even if it's a stranger, -Dialogue: 0,0:19:50.98,0:19:53.08,para-main,Mitsu,0000,0000,0000,,once I come to know them, -Dialogue: 0,0:19:53.08,0:19:54.66,para-main,Mitsu,0000,0000,0000,,I can't just abandon them. -Dialogue: 0,0:19:54.66,0:19:57.20,para-main,Mitsu,0000,0000,0000,,That's what it means to be human. -Dialogue: 0,0:19:57.20,0:19:58.73,para-main,Mitsu,0000,0000,0000,,But you... -Dialogue: 0,0:20:00.87,0:20:05.52,para-main,Mitsu,0000,0000,0000,,I don't know how much time you have left, -Dialogue: 0,0:20:05.52,0:20:11.89,para-main,Mitsu,0000,0000,0000,,but give some thought to as many things, \Nas many ideas, as you can come up with. -Dialogue: 0,0:20:12.80,0:20:16.47,para-main,Mitsu,0000,0000,0000,,If you throw everything away, that's the end. -Dialogue: 0,0:20:17.05,0:20:20.10,para-main,Mitsu,0000,0000,0000,,Don't give up, no matter what, -Dialogue: 0,0:20:20.10,0:20:21.78,para-main,Mitsu,0000,0000,0000,,and be flexible. -Dialogue: 0,0:20:31.45,0:20:32.36,para-main,Mitsu,0000,0000,0000,,Wait. -Dialogue: 0,0:20:32.90,0:20:35.81,para-main,Mitsu,0000,0000,0000,,Isn't there something useful \Nyou can take with you? -Dialogue: 0,0:20:35.81,0:20:37.09,para-main,Mitsu,0000,0000,0000,,Like a weapon? -Dialogue: 0,0:20:41.43,0:20:42.84,para-main,S,0000,0000,0000,,This, then. -Dialogue: 0,0:20:42.84,0:20:46.42,para-main,Mitsu,0000,0000,0000,,What, that? It's all rusty. -Dialogue: 0,0:20:46.42,0:20:48.60,para-main,Mitsu,0000,0000,0000,,But I guess it's better than nothing. -Dialogue: 0,0:21:02.69,0:21:03.88,para-internal,Mitsu,0000,0000,0000,,Dear... -Dialogue: 0,0:21:04.62,0:21:06.32,para-internal,Mitsu,0000,0000,0000,,Please keep him safe. -Dialogue: 0,0:21:15.06,0:21:19.51,para-internal,S,0000,0000,0000,,If Gotou's adopted a totally \Ndifferent human appearance, -Dialogue: 0,0:21:19.51,0:21:21.48,para-internal,S,0000,0000,0000,,there'll be no way for me to recognize him. -Dialogue: 0,0:21:21.96,0:21:25.53,para-internal,S,0000,0000,0000,,But I'll cross that bridge when I come to it! -Dialogue: 0,0:22:46.84,0:22:51.03,para-next-ep,Sign 2248,0000,0000,0000,,{\fad(700,1)}Life and Oath -Dialogue: 0,0:22:46.93,0:22:47.67,para-main,S,0000,0000,0000,,Next time: -Dialogue: 0,0:22:48.64,0:22:49.81,para-main,S,0000,0000,0000,,"Life and Oath." diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt deleted file mode 100644 index b6352e7b5..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt +++ /dev/null @@ -1,32 +0,0 @@ -WEBVTT - -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -00:00:15.712 --> 00:00:17.399 -Oh my god, Watch out!
It's coming!! - -00:00:25.712 --> 00:00:30.399 -[Bird noises] - -00:00:31.000 --> 00:00:31.999 -This text is RED and has not been positioned. - -00:00:32.000 --> 00:00:32.999 -This is a
new line, as is
this - -00:00:33.000 --> 00:00:33.999 -This contains nested bold, italic, underline and strike-through HTML tags - -00:00:34.000 --> 00:00:34.999 -Unclosed but supported HTML tags are left in, SSA italics aren't - -00:00:35.000 --> 00:00:35.999 -<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed. - -00:00:36.000 --> 00:00:36.999 -Multiple SSA tags are stripped - -00:00:37.000 --> 00:00:37.999 -Greater than (<) and less than (>) are shown - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt deleted file mode 100644 index 1ce811bcb..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt +++ /dev/null @@ -1,44 +0,0 @@ - - -1 -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -2 -00:00:15,712 --> 00:00:17,399 X1:000 X2:000 Y1:050 Y2:100 -Oh my god, Watch out! -It's coming!! - -3 -00:00:25,712 --> 00:00:30,399 -[Bird noises] - -4 -00:00:31,000 --> 00:00:31,999 -This text is RED and has not been {\pos(142,120)}positioned. - -5 -00:00:32,000 --> 00:00:32,999 -This is a\nnew line, as is\Nthis - -6 -00:00:33,000 --> 00:00:33,999 -This contains nested bold, italic, underline and strike-through HTML tags - -7 -00:00:34,000 --> 00:00:34,999 -Unclosed but supported HTML tags are left in, {\i1} SSA italics aren't - -8 -00:00:35,000 --> 00:00:35,999 -Unsupported HTML tags are escaped and left in, even if not closed. - -9 -00:00:36,000 --> 00:00:36,999 -Multiple {\bord-3.7\clip(1,m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0)\pos(142,120)\t(0,500,\fscx100\fscy100)\b1\c&H000000&}SSA tags are stripped - -10 -00:00:37,000 --> 00:00:37,999 -Greater than (<) and less than (>) are shown - - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs deleted file mode 100644 index 2d25bcb14..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class VttWriterTest { - [TestMethod] - public void TestWrite() { - var infoSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!
It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is RED and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a
new line, as is
this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested bold, italic, underline and strike-through HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new VttWriter(); - - if(File.Exists("testVTT.vtt")) - File.Delete("testVTT.vtt"); - using (var file = File.OpenWrite("testVTT.vtt")) - { - sut.Write(infoSubs, file, CancellationToken.None); - } - - var result = File.ReadAllText("testVTT.vtt"); - var expectedText = File.ReadAllText(@"MediaEncoding\Subtitles\TestSubtitles\expected.vtt"); - - Assert.AreEqual(expectedText, result); - } - } -} diff --git a/MediaBrowser.Tests/Properties/AssemblyInfo.cs b/MediaBrowser.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 1bd3ef5d6..000000000 --- a/MediaBrowser.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MediaBrowser.Tests/app.config b/MediaBrowser.Tests/app.config deleted file mode 100644 index 5c79b167f..000000000 --- a/MediaBrowser.Tests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 39839e273..d23ca1cdb 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -53,6 +53,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +155,10 @@ Global {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.Build.0 = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -176,4 +184,7 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection EndGlobal diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj new file mode 100644 index 000000000..449aaa1a5 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs new file mode 100644 index 000000000..5fa86f3bd --- /dev/null +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Cryptography; +using Xunit; +using static MediaBrowser.Common.HexHelper; + +namespace Jellyfin.Common.Tests +{ + public class PasswordHashTests + { + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + "PBKDF2", + "", + "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ParseTest(string passwordHash, string id, string salt, string hash) + { + var pass = PasswordHash.Parse(passwordHash); + Assert.Equal(id, pass.Id); + Assert.Equal(salt, ToHexString(pass.Salt)); + Assert.Equal(hash, ToHexString(pass.Hash)); + } + + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ToStringTest(string passwordHash) + { + Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString()); + } + } +} -- cgit v1.2.3 From c9820d30edf1cb8fa99a52ec72b6571d6d4506f7 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 20 Sep 2019 12:42:08 +0200 Subject: Fix multiple mistakes and warnings --- BDInfo/BDROM.cs | 9 +---- Emby.Server.Implementations/Dto/DtoService.cs | 10 ++--- .../HttpServer/Security/AuthService.cs | 2 +- .../Library/CoreResolutionIgnoreRule.cs | 1 - Emby.Server.Implementations/Library/UserManager.cs | 4 +- .../Library/UserViewManager.cs | 4 +- .../LiveTv/EmbyTV/EncodedRecorder.cs | 3 -- .../LiveTv/Listings/SchedulesDirect.cs | 37 ++++++++---------- .../LiveTv/LiveTvManager.cs | 45 ++++++++++------------ .../TunerHosts/HdHomerun/HdHomerunManager.cs | 4 +- .../Services/StringMapTypeDeserializer.cs | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Api/UserLibrary/ItemsService.cs | 4 +- MediaBrowser.Common/Extensions/BaseExtensions.cs | 12 +++--- .../Extensions/CollectionExtensions.cs | 19 ++++++++- MediaBrowser.Controller/Entities/BaseItem.cs | 21 ++++++---- .../MediaEncoding/EncodingHelper.cs | 3 -- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 2 - .../Subtitles/SubtitleEncoder.cs | 8 ++-- .../Savers/EpisodeNfoSaver.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 4 -- 21 files changed, 94 insertions(+), 104 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/BDInfo/BDROM.cs b/BDInfo/BDROM.cs index 6759ed55a..3a0c14ffd 100644 --- a/BDInfo/BDROM.cs +++ b/BDInfo/BDROM.cs @@ -212,7 +212,6 @@ namespace BDInfo public void Scan() { - var errorStreamClipFiles = new List(); foreach (var streamClipFile in StreamClipFiles.Values) { try @@ -221,7 +220,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamClipFiles.Add(streamClipFile); if (StreamClipFileScanError != null) { if (StreamClipFileScanError(streamClipFile, ex)) @@ -250,7 +248,6 @@ namespace BDInfo StreamFiles.Values.CopyTo(streamFiles, 0); Array.Sort(streamFiles, CompareStreamFiles); - var errorPlaylistFiles = new List(); foreach (var playlistFile in PlaylistFiles.Values) { try @@ -259,7 +256,6 @@ namespace BDInfo } catch (Exception ex) { - errorPlaylistFiles.Add(playlistFile); if (PlaylistFileScanError != null) { if (PlaylistFileScanError(playlistFile, ex)) @@ -275,7 +271,6 @@ namespace BDInfo } } - var errorStreamFiles = new List(); foreach (var streamFile in streamFiles) { try @@ -296,7 +291,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamFiles.Add(streamFile); if (StreamFileScanError != null) { if (StreamFileScanError(streamFile, ex)) @@ -431,7 +425,7 @@ namespace BDInfo { return 1; } - else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null)) + else if ((x != null && x.FileInfo != null) && (y == null || y.FileInfo == null)) { return -1; } @@ -451,6 +445,5 @@ namespace BDInfo } } } - } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 75192a8f1..a3201f0bc 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -218,14 +218,12 @@ namespace Emby.Server.Implementations.Dto AttachUserSpecificInfo(dto, item, user, options); } - if (item is IHasMediaSources hasMediaSources) + if (item is IHasMediaSources + && options.ContainsField(ItemFields.MediaSources)) { - if (options.ContainsField(ItemFields.MediaSources)) - { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); - NormalizeMediaSourceContainers(dto); - } + NormalizeMediaSourceContainers(dto); } if (options.ContainsField(ItemFields.Studios)) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 3d3f67ca2..93a61fe67 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var user = auth.User; - if (user == null & !auth.UserId.Equals(Guid.Empty)) + if (user == null && auth.UserId != Guid.Empty) { throw new SecurityException("User with Id " + auth.UserId + " not found"); } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f1ae2fc9c..8bdb38784 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -57,7 +57,6 @@ namespace Emby.Server.Implementations.Library } var filename = fileInfo.Name; - var path = fileInfo.FullName; // Ignore hidden files on UNIX if (Environment.OSVersion.Platform != PlatformID.Win32NT diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ac6b4a209..52b2f56ff 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -273,14 +273,12 @@ namespace Emby.Server.Implementations.Library var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; - string updatedUsername = null; IAuthenticationProvider authenticationProvider = null; if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.authenticationProvider; - updatedUsername = authResult.username; success = authResult.success; } else @@ -288,7 +286,7 @@ namespace Emby.Server.Implementations.Library // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.authenticationProvider; - updatedUsername = authResult.username; + string updatedUsername = authResult.username; success = authResult.success; if (success diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 4d79cae13..88e2a8fa6 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations.Library if (!parentId.Equals(Guid.Empty)) { var parentItem = _libraryManager.GetItemById(parentId); - if (parentItem is Channel parentItemChannel) + if (parentItem is Channel) { return _channelManager.GetLatestChannelItemsInternal( new InternalItemsQuery(user) @@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.Library IncludeItemTypes = request.IncludeItemTypes, EnableTotalRecordCount = false }, - CancellationToken.None).Result.Items; + CancellationToken.None).GetAwaiter().GetResult().Items; } if (parentItem is Folder parent) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 3cc0541e7..cc9c8e5d2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -208,9 +208,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private static string GetAudioArgs(MediaSourceInfo mediaSource) { - var mediaStreams = mediaSource.MediaStreams ?? new List(); - var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; - return "-codec:a:0 copy"; //var audioChannels = 2; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index f5dffc22a..9a4c91d0b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.Listings { @@ -41,6 +40,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings private string UserAgent => _appHost.ApplicationUserAgent; + /// + public string Name => "Schedules Direct"; + + /// + public string Type => nameof(SchedulesDirect); + private static List GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc) { var dates = new List(); @@ -103,7 +108,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; using (var response = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var reader = new StreamReader(response.Content)) { var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync>(response.Content).ConfigureAwait(false); _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); @@ -122,7 +126,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]"; using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var innerReader = new StreamReader(innerResponse.Content)) { var programDetails = await _jsonSerializer.DeserializeFromStreamAsync>(innerResponse.Content).ConfigureAwait(false); var programDict = programDetails.ToDictionary(p => p.programID, y => y); @@ -152,14 +155,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); - const double desiredAspect = 0.666666667; + const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, desiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, desiredAspect); + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - const double wideAspect = 1.77777778; + const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, wideAspect); + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); // Don't supply the same image twice if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) @@ -167,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.thumbImage = null; } - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, wideAspect); + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -178,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + return programsInfo; } } @@ -185,12 +189,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static int GetSizeOrder(ScheduleDirect.ImageData image) { - if (!string.IsNullOrWhiteSpace(image.height)) + if (!string.IsNullOrWhiteSpace(image.height) + && int.TryParse(image.height, out int value)) { - if (int.TryParse(image.height, out int value)) - { - return value; - } + return value; } return 0; @@ -736,16 +738,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; - using (var response = await _httpClient.SendAsync(httpOptions, "PUT")) + using (await _httpClient.SendAsync(httpOptions, "PUT")) { } } - public string Name => "Schedules Direct"; - - public static string TypeName = "SchedulesDirect"; - public string Type => TypeName; - private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(info.ListingsId)) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ee975e19a..89b92c999 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -60,16 +60,6 @@ namespace Emby.Server.Implementations.LiveTv private IListingsProvider[] _listingProviders = Array.Empty(); private readonly IFileSystem _fileSystem; - public event EventHandler> SeriesTimerCancelled; - public event EventHandler> TimerCancelled; - public event EventHandler> TimerCreated; - public event EventHandler> SeriesTimerCreated; - - public string GetEmbyTvActiveRecordingPath(string id) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); - } - public LiveTvManager( IServerApplicationHost appHost, IServerConfigurationManager config, @@ -102,17 +92,34 @@ namespace Emby.Server.Implementations.LiveTv _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager); } + public event EventHandler> SeriesTimerCancelled; + + public event EventHandler> TimerCancelled; + + public event EventHandler> TimerCreated; + + public event EventHandler> SeriesTimerCreated; + /// /// Gets the services. /// /// The services. public IReadOnlyList Services => _services; + public ITunerHost[] TunerHosts => _tunerHosts; + + public IListingsProvider[] ListingProviders => _listingProviders; + private LiveTvOptions GetConfiguration() { return _config.GetConfiguration("livetv"); } + public string GetEmbyTvActiveRecordingPath(string id) + { + return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); + } + /// /// Adds the parts. /// @@ -130,13 +137,13 @@ namespace Emby.Server.Implementations.LiveTv { if (service is EmbyTV.EmbyTV embyTv) { - embyTv.TimerCreated += EmbyTv_TimerCreated; - embyTv.TimerCancelled += EmbyTv_TimerCancelled; + embyTv.TimerCreated += OnEmbyTvTimerCreated; + embyTv.TimerCancelled += OnEmbyTvTimerCancelled; } } } - private void EmbyTv_TimerCancelled(object sender, GenericEventArgs e) + private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs e) { var timerId = e.Argument; @@ -149,10 +156,9 @@ namespace Emby.Server.Implementations.LiveTv }); } - private void EmbyTv_TimerCreated(object sender, GenericEventArgs e) + private void OnEmbyTvTimerCreated(object sender, GenericEventArgs e) { var timer = e.Argument; - var service = sender as ILiveTvService; TimerCreated?.Invoke(this, new GenericEventArgs { @@ -164,10 +170,6 @@ namespace Emby.Server.Implementations.LiveTv }); } - public ITunerHost[] TunerHosts => _tunerHosts; - - public IListingsProvider[] ListingProviders => _listingProviders; - public List GetTunerHostTypes() { return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair @@ -966,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv private async Task AddRecordingInfo(IEnumerable> programs, CancellationToken cancellationToken) { - var timers = new Dictionary>(); - var seriesTimers = new Dictionary>(); - IReadOnlyList timerList = null; IReadOnlyList seriesTimerList = null; @@ -1601,8 +1600,6 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.Id)) { - var guid = new Guid(query.Id); - timers = timers .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 3699b988c..9702392b2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -424,14 +424,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return false; } - var nameTag = buf[offset++]; + offset++; // Name Tag var nameLength = buf[offset++]; // skip the name field to get to value for return offset += nameLength; - var valueTag = buf[offset++]; + offset++; // Value Tag var valueLength = buf[offset++]; diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index c27eb7686..23e22afd5 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Services { PropertySetFn = propertySetFn; PropertyParseStringFn = propertyParseStringFn; - PropertyType = PropertyType; + PropertyType = propertyType; } public Action PropertySetFn { get; private set; } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 35f0c84cb..fa3e9cb35 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -22,7 +22,7 @@ - + diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index ada540ba6..b4a302648 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -455,9 +455,7 @@ namespace MediaBrowser.Api.UserLibrary IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, Name = i, Limit = 1 - - }).Select(albumId => albumId); - + }); }).ToArray(); } diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 40c16b957..33473c2be 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -14,20 +14,20 @@ namespace MediaBrowser.Common.Extensions /// Strips the HTML. ///
/// The HTML string. - /// System.String. + /// . public static string StripHtml(this string htmlString) { // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net - const string pattern = @"<(.|\n)*?>"; + const string Pattern = @"<(.|\n)*?>"; - return Regex.Replace(htmlString, pattern, string.Empty).Trim(); + return Regex.Replace(htmlString, Pattern, string.Empty).Trim(); } /// - /// Gets the M d5. + /// Gets the Md5. /// - /// The STR. - /// Guid. + /// The string. + /// . public static Guid GetMD5(this string str) { using (var provider = MD5.Create()) diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs index 3bc0295a0..75b9f59f8 100644 --- a/MediaBrowser.Common/Extensions/CollectionExtensions.cs +++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs @@ -5,13 +5,28 @@ namespace MediaBrowser.Common.Extensions // The MS CollectionExtensions are only available in netcoreapp public static class CollectionExtensions { - public static TValue GetValueOrDefault (this IReadOnlyDictionary dictionary, TKey key) + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) { dictionary.TryGetValue(key, out var ret); return ret; } - // REVIEW: Inline? + /// + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// The current collection that is the source of the elements. + /// The list that is the destination of the elements copied from the current collection. + /// A 32-bit integer that represents the index in destination at which copying begins. + /// + public static void CopyTo(this IReadOnlyList source, IList destination, int index = 0) + { + for (int i = 0; i < source.Count; i++) + { + destination[index + i] = source[i]; + } + } + /// /// Copies all the elements of the current collection to the specified list /// starting at the specified destination array index. The index is specified as a 32-bit integer. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0e9f7ee44..369f63b13 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2045,7 +2045,7 @@ namespace MediaBrowser.Controller.Entities if (itemByPath == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; @@ -2057,7 +2057,7 @@ namespace MediaBrowser.Controller.Entities if (item == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; @@ -2085,14 +2085,17 @@ namespace MediaBrowser.Controller.Entities if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) { - if (current.Length == 0) + int curLen = current.Length; + if (curLen == 0) { Studios = new[] { name }; } else { - var list = - Studios = current.Concat(new[] { name }).ToArray(); + var newArr = new string[curLen + 1]; + current.CopyTo(newArr, 0); + newArr[curLen] = name; + Studios = newArr; } } } @@ -2231,8 +2234,12 @@ namespace MediaBrowser.Controller.Entities else { - var currentCount = ImageInfos.Length; - ImageInfos = ImageInfos.Concat(new[] { image }).ToArray(); + var current = ImageInfos; + var currentCount = current.Length; + var newArr = new ItemImageInfo[currentCount + 1]; + current.CopyTo(newArr, 0); + current[currentCount] = image; + ImageInfos = current; } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 841205d0c..eb3d2ab81 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1252,9 +1252,6 @@ namespace MediaBrowser.Controller.MediaEncoding { if (request.AudioBitRate.HasValue) { - // Make sure we don't request a bitrate higher than the source - var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; - // Don't encode any higher than this return Math.Min(384000, request.AudioBitRate.Value); } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 19009e577..bd727bcdf 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -158,8 +158,6 @@ namespace MediaBrowser.LocalMetadata.Savers /// Task. public static void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config) { - var writtenProviderIds = new HashSet(StringComparer.OrdinalIgnoreCase); - if (!string.IsNullOrEmpty(item.OfficialRating)) { writer.WriteElementString("ContentRating", item.OfficialRating); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9ddfb9b01..d5fa76c3a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -506,12 +506,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = string.Format("ffmpeg subtitle conversion failed for {Path}", inputPath); + _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath); - _logger.LogError(msg); - - throw new Exception(msg); + throw new Exception( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } + await SetAssFont(outputPath).ConfigureAwait(false); _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index aa28fded1..091c1957e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.XbmcMetadata.Savers /// public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - => !item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType; + => item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType; /// protected override void WriteCustomElements(BaseItem item, XmlWriter writer) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 7f3e56394..53b740052 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -86,7 +86,6 @@ namespace Rssdp.Infrastructure ThrowIfDisposed(); - var minCacheTime = TimeSpan.Zero; bool wasAdded = false; lock (_Devices) { @@ -94,7 +93,6 @@ namespace Rssdp.Infrastructure { _Devices.Add(device); wasAdded = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } @@ -120,14 +118,12 @@ namespace Rssdp.Infrastructure if (device == null) throw new ArgumentNullException(nameof(device)); bool wasRemoved = false; - var minCacheTime = TimeSpan.Zero; lock (_Devices) { if (_Devices.Contains(device)) { _Devices.Remove(device); wasRemoved = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } -- cgit v1.2.3