diff options
21 files changed, 656 insertions, 634 deletions
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 217ea3a4b..66c634150 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -16,6 +16,8 @@ namespace Emby.Dlna.PlayTo private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; private const string FriendlyName = "Jellyfin"; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; @@ -25,7 +27,8 @@ namespace Emby.Dlna.PlayTo _config = config; } - public async Task<XDocument> SendCommandAsync(string baseUrl, + public async Task<XDocument> SendCommandAsync( + string baseUrl, DeviceService service, string command, string postData, @@ -35,12 +38,20 @@ namespace Emby.Dlna.PlayTo var cancellationToken = CancellationToken.None; var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); - using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken) + using (var response = await PostSoapDataAsync( + url, + $"\"{service.ServiceType}#{command}\"", + postData, + header, + logRequest, + cancellationToken) .ConfigureAwait(false)) using (var stream = response.Content) using (var reader = new StreamReader(stream, Encoding.UTF8)) { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); + return XDocument.Parse( + await reader.ReadToEndAsync().ConfigureAwait(false), + LoadOptions.PreserveWhitespace); } } @@ -58,9 +69,8 @@ namespace Emby.Dlna.PlayTo return baseUrl + serviceUrl; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public async Task SubscribeAsync(string url, + public async Task SubscribeAsync( + string url, string ip, int port, string localIp, @@ -101,14 +111,12 @@ namespace Emby.Dlna.PlayTo options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) + using (var stream = response.Content) + using (var reader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) - { - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); - } - } + return XDocument.Parse( + await reader.ReadToEndAsync().ConfigureAwait(false), + LoadOptions.PreserveWhitespace); } } @@ -122,7 +130,7 @@ namespace Emby.Dlna.PlayTo { if (soapAction[0] != '\"') { - soapAction = '\"' + soapAction + '\"'; + soapAction = $"\"{soapAction}\""; } var options = new HttpRequestOptions 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..6ab3d1bb1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -315,8 +315,6 @@ namespace Emby.Server.Implementations private IMediaSourceManager MediaSourceManager { get; set; } - private IPlaylistManager PlaylistManager { get; set; } - private readonly IConfiguration _configuration; /// <summary> @@ -325,14 +323,6 @@ namespace Emby.Server.Implementations /// <value>The installation manager.</value> protected IInstallationManager InstallationManager { get; private set; } - /// <summary> - /// Gets or sets the zip client. - /// </summary> - /// <value>The zip client.</value> - protected IZipClient ZipClient { get; private set; } - - protected IHttpResultFactory HttpResultFactory { get; private set; } - protected IAuthService AuthService { get; private set; } public IStartupOptions StartupOptions { get; } @@ -680,8 +670,6 @@ namespace Emby.Server.Implementations await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); } - public static IStreamHelper StreamHelper { get; set; } - /// <summary> /// Registers resources that classes will depend on /// </summary> @@ -725,8 +713,7 @@ namespace Emby.Server.Implementations ProcessFactory = new ProcessFactory(); serviceCollection.AddSingleton(ProcessFactory); - ApplicationHost.StreamHelper = new StreamHelper(); - serviceCollection.AddSingleton(StreamHelper); + serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper)); serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider)); @@ -735,11 +722,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager)); - ZipClient = new ZipClient(); - serviceCollection.AddSingleton(ZipClient); + serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient)); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper); - serviceCollection.AddSingleton(HttpResultFactory); + serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory)); serviceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); @@ -770,7 +755,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<UserManager>(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); @@ -836,8 +822,7 @@ namespace Emby.Server.Implementations CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); serviceCollection.AddSingleton(CollectionManager); - PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LoggerFactory, UserManager, ProviderManager); - serviceCollection.AddSingleton(PlaylistManager); + serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager)); LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager); serviceCollection.AddSingleton(LiveTvManager); 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<User> 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<double> 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; } + /// <inheritdoc /> + public string Name => "Default Password Reset Provider"; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) { SerializablePasswordReset spr; - HashSet<string> usersreset = new HashSet<string>(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + List<string> usersreset = new List<string>(); + 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 } } + /// <inheritdoc /> public async Task<ForgotPasswordResult> 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; @@ -41,34 +40,19 @@ namespace Emby.Server.Implementations.Library public class UserManager : IUserManager { /// <summary> - /// Gets the users. - /// </summary> - /// <value>The users.</value> - public IEnumerable<User> Users => _users; - - private User[] _users; - - /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly object _policySyncLock = new object(); /// <summary> /// Gets the active user repository /// </summary> /// <value>The user repository.</value> - private IUserRepository UserRepository { get; set; } - public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; - + private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; private readonly Func<IImageProcessor> _imageProcessorFactory; @@ -76,6 +60,8 @@ namespace Emby.Server.Implementations.Library private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private ConcurrentDictionary<Guid, User> _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<UserManager> 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<User>(); + _users = null; + } + + public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; + + /// <summary> + /// Occurs when [user updated]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserUpdated; + + public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; + + public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; + + public event EventHandler<GenericEventArgs<User>> UserLockedOut; + + public event EventHandler<GenericEventArgs<User>> UserCreated; + + /// <summary> + /// Occurs when [user deleted]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserDeleted; + + /// <inheritdoc /> + public IEnumerable<User> Users => _users.Values; + + /// <inheritdoc /> + public IEnumerable<Guid> UsersIds => _users.Keys; + + /// <summary> + /// Called when [user updated]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + } + + /// <summary> + /// Called when [user deleted]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); } public NameIdPair[] GetAuthenticationProviders() @@ -137,7 +165,7 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders) + public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); @@ -150,54 +178,21 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); } - #region UserUpdated Event /// <summary> - /// Occurs when [user updated]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserUpdated; - public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; - public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; - public event EventHandler<GenericEventArgs<User>> UserLockedOut; - - /// <summary> - /// Called when [user updated]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// <summary> - /// Occurs when [user deleted]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserDeleted; - /// <summary> - /// Called when [user deleted]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - - /// <summary> - /// Gets a User by Id + /// Gets a User by Id. /// </summary> /// <param name="id">The id.</param> /// <returns>User.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentException"></exception> 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; } /// <summary> @@ -206,15 +201,13 @@ namespace Emby.Server.Implementations.Library /// <param name="id">The identifier.</param> /// <returns>User.</returns> 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 } /// <summary> - /// Loads the users from the repository + /// Loads the users from the repository. /// </summary> - /// <returns>IEnumerable{User}.</returns> - private List<User> 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<Guid, User>( + users.Select(x => new KeyValuePair<Guid, User>(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> { user }; + _users = new ConcurrentDictionary<Guid, User>(); + _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<GenericEventArgs<User>> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - /// <summary> /// Creates the user. /// </summary> @@ -751,7 +754,7 @@ namespace Emby.Server.Implementations.Library /// <returns>User.</returns> /// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentException"></exception> - public async Task<User> 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<User> { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); - return user; - } - finally - { - _userListLock.Release(); - } + return user; } /// <summary> @@ -799,57 +791,59 @@ namespace Emby.Server.Implementations.Library /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">user</exception> /// <exception cref="ArgumentException"></exception> - 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); } /// <summary> @@ -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/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index ed254accb..85754ca8b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -31,6 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; + private readonly IStreamHelper _streamHelper; public HdHomerunHost( IServerConfigurationManager config, @@ -40,29 +42,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHttpClient httpClient, IServerApplicationHost appHost, ISocketFactory socketFactory, - INetworkManager networkManager) + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; + _streamHelper = streamHelper; } public string Name => "HD Homerun"; - public override string Type => DeviceType; - - public static string DeviceType => "hdhomerun"; + public override string Type => "hdhomerun"; protected override string ChannelIdPrefix => "hdhr_"; private string GetChannelId(TunerHostInfo info, Channels i) - { - var id = ChannelIdPrefix + i.GuideNumber; - - return id; - } + => ChannelIdPrefix + i.GuideNumber; private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) { @@ -74,19 +72,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun CancellationToken = cancellationToken, BufferContent = false }; - using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - if (info.ImportFavoritesOnly) - { - lineup = lineup.Where(i => i.Favorite).ToList(); - } + using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) + { + var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - return lineup.Where(i => !i.DRM).ToList(); + if (info.ImportFavoritesOnly) + { + lineup = lineup.Where(i => i.Favorite).ToList(); } + + return lineup.Where(i => !i.DRM).ToList(); } } @@ -139,23 +136,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - - }, "GET").ConfigureAwait(false)) + }, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); + var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); - if (!string.IsNullOrEmpty(cacheKey)) + if (!string.IsNullOrEmpty(cacheKey)) + { + lock (_modelCache) { - lock (_modelCache) - { - _modelCache[cacheKey] = discoverResponse; - } + _modelCache[cacheKey] = discoverResponse; } - - return discoverResponse; } + + return discoverResponse; } } catch (HttpException ex) @@ -186,36 +180,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var stream = await _httpClient.Get(new HttpRequestOptions() + using (var response = await _httpClient.SendAsync(new HttpRequestOptions() { Url = string.Format("{0}/tuners.html", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - })) + }, HttpMethod.Get)) + using (var stream = response.Content) + using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) { var tuners = new List<LiveTvTunerInfo>(); - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + while (!sr.EndOfStream) { - while (!sr.EndOfStream) + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel")) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } + tuners.Add(new LiveTvTunerInfo { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); } } + return tuners; } } @@ -245,6 +239,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bufferIndex++; } } + return new string(buffer, 0, bufferIndex); } @@ -256,7 +251,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var uri = new Uri(GetApiUrl(info)); - using (var manager = new HdHomerunManager(Logger)) + using (var manager = new HdHomerunManager()) { // Legacy HdHomeruns are IPv4 only var ipInfo = IPAddress.Parse(uri.Host); @@ -276,6 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } } + return tuners; } @@ -434,12 +430,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { videoCodec = channelInfo.VideoCodec; } + string audioCodec = channelInfo.AudioCodec; if (!videoBitrate.HasValue) { videoBitrate = isHd ? 15000000 : 2000000; } + int? audioBitrate = isHd ? 448000 : 192000; // normalize @@ -461,6 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { id = "native"; } + id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture); var mediaSource = new MediaSourceInfo @@ -527,29 +526,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } else { - try - { - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - if (modelInfo != null && modelInfo.SupportsTranscoding) + if (modelInfo != null && modelInfo.SupportsTranscoding) + { + if (info.AllowHWTranscoding) { - if (info.AllowHWTranscoding) - { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); - } + list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); } - } - catch - { + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); } if (list.Count == 0) @@ -582,7 +574,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) { - return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _socketFactory, + _networkManager, + _streamHelper); } var enableHttpStream = true; @@ -599,10 +603,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } mediaSource.Path = httpUrl; - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); - } - - return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper); + } + + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _socketFactory, + _networkManager, + _streamHelper); } public async Task Validate(TunerHostInfo info) @@ -701,9 +717,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun catch (OperationCanceledException) { } - catch + catch (Exception ex) { // Socket timeout indicates all messages have been received. + Logger.LogError(ex, "Error while sending discovery message"); } } @@ -718,21 +735,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = url }; - try - { - var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - - hostInfo.DeviceId = modelInfo.DeviceID; - hostInfo.FriendlyName = modelInfo.FriendlyName; + var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - return hostInfo; - } - catch - { - // logged at lower levels - } + hostInfo.DeviceId = modelInfo.DeviceID; + hostInfo.FriendlyName = modelInfo.FriendlyName; - return null; + return hostInfo; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index c19552428..3699b988c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Net.Sockets; using System.Text; @@ -8,13 +9,12 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.LiveTv; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public interface IHdHomerunChannelCommands { - IEnumerable<Tuple<string, string>> GetCommands(); + IEnumerable<(string, string)> GetCommands(); } public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands @@ -33,16 +33,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) - commands.Add(Tuple.Create("channel", _channel)); + { + yield return ("channel", _channel); + } if (!string.IsNullOrEmpty(_program)) - commands.Add(Tuple.Create("program", _program)); - return commands; + { + yield return ("program", _program); + } } } @@ -57,29 +58,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _profile = profile; } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) { - if (!string.IsNullOrEmpty(_profile) && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(_profile) + && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) { - commands.Add(Tuple.Create("vchannel", string.Format("{0} transcode={1}", _channel, _profile))); + yield return ("vchannel", $"{_channel} transcode={_profile}"); } else { - commands.Add(Tuple.Create("vchannel", _channel)); + yield return ("vchannel", _channel); } } - - return commands; } } public class HdHomerunManager : IDisposable { public const int HdHomeRunPort = 65001; + // Message constants private const byte GetSetName = 3; private const byte GetSetValue = 4; @@ -87,19 +86,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private const ushort GetSetRequest = 4; private const ushort GetSetReply = 5; - private readonly ILogger _logger; - private uint? _lockkey = null; private int _activeTuner = -1; private IPEndPoint _remoteEndPoint; private TcpClient _tcpClient; - public HdHomerunManager(ILogger logger) - { - _logger = logger; - } - public void Dispose() { using (var socket = _tcpClient) @@ -108,8 +100,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { _tcpClient = null; - var task = StopStreaming(socket); - Task.WaitAll(task); + StopStreaming(socket).GetAwaiter().GetResult(); } } } @@ -173,20 +164,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out var returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { continue; } var commandList = commands.GetCommands(); - foreach (Tuple<string, string> command in commandList) + foreach (var command in commandList) { var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -198,8 +191,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -231,13 +225,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); try { - foreach (Tuple<string, string> command in commandList) + foreach (var command in commandList) { var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out string returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { return; } @@ -264,21 +259,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue) { - _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue); - var stream = client.GetStream(); var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length, CancellationToken.None).ConfigureAwait(false); + await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false); var buffer = ArrayPool<byte>.Shared.Rent(8192); try { - await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); _lockkey = null; - await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, CancellationToken.None).ConfigureAwait(false); - await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); } finally { @@ -288,7 +281,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static byte[] CreateGetMessage(int tuner, string name) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length var message = new byte[messageLength]; @@ -311,12 +304,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); - var byteValue = Encoding.UTF8.GetBytes(string.Format("{0}\0", value)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); + var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value)); int messageLength = byteName.Length + byteValue.Length + 12; if (lockkey.HasValue) + { messageLength += 6; + } var message = new byte[messageLength]; @@ -324,21 +319,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bool flipEndian = BitConverter.IsLittleEndian; - message[offset] = GetSetValue; - offset++; - message[offset] = Convert.ToByte(byteValue.Length); - offset++; + message[offset++] = GetSetValue; + message[offset++] = Convert.ToByte(byteValue.Length); Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length); offset += byteValue.Length; if (lockkey.HasValue) { - message[offset] = GetSetLockkey; - offset++; - message[offset] = (byte)4; - offset++; + message[offset++] = GetSetLockkey; + message[offset++] = 4; var lockKeyBytes = BitConverter.GetBytes(lockkey.Value); if (flipEndian) + { Array.Reverse(lockKeyBytes); + } + Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); offset += 4; } @@ -346,7 +340,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // calculate crc and insert at the end of the message var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); if (flipEndian) + { Array.Reverse(crcBytes); + } + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return message; @@ -375,10 +372,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun offset += 2; // insert tag name and length - message[offset] = GetSetName; - offset++; - message[offset] = Convert.ToByte(byteName.Length); - offset++; + message[offset++] = GetSetName; + message[offset++] = Convert.ToByte(byteName.Length); // insert name string Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length); @@ -392,7 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun returnVal = string.Empty; if (numBytes < 4) + { return false; + } var flipEndian = BitConverter.IsLittleEndian; int offset = 0; @@ -400,45 +397,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length); if (flipEndian) + { Array.Reverse(msgTypeBytes); + } var msgType = BitConverter.ToUInt16(msgTypeBytes, 0); offset += 2; if (msgType != GetSetReply) + { return false; + } byte[] msgLengthBytes = new byte[2]; Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length); if (flipEndian) + { Array.Reverse(msgLengthBytes); + } var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0); offset += 2; if (numBytes < msgLength + 8) + { return false; + } - var nameTag = buf[offset]; - offset++; + var nameTag = buf[offset++]; - var nameLength = buf[offset]; - offset++; + var nameLength = buf[offset++]; // skip the name field to get to value for return offset += nameLength; - var valueTag = buf[offset]; - offset++; + var valueTag = buf[offset++]; - var valueLength = buf[offset]; - offset++; + var valueLength = buf[offset++]; returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator return true; } - private class HdHomerunCrc + private static class HdHomerunCrc { private static uint[] crc_table = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, @@ -510,15 +511,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var hash = 0xffffffff; for (var i = 0; i < numBytes; i++) + { hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff]; + } var tmp = ~hash & 0xffffffff; var b0 = tmp & 0xff; var b1 = (tmp >> 8) & 0xff; var b2 = (tmp >> 16) & 0xff; var b3 = (tmp >> 24) & 0xff; - hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; - return hash; + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 1d79a5f96..fbbab07f8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Net; @@ -18,6 +19,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider { + private const int RtpHeaderBytes = 12; + private readonly IServerApplicationHost _appHost; private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory; @@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHdHomerunChannelCommands channelCommands, int numTuners, IFileSystem fileSystem, - IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, MediaBrowser.Model.Net.ISocketFactory socketFactory, - INetworkManager networkManager) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + INetworkManager networkManager, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _appHost = appHost; _socketFactory = socketFactory; @@ -80,12 +83,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var udpClient = _socketFactory.CreateUdpSocket(localPort); - var hdHomerunManager = new HdHomerunManager(Logger); + var hdHomerunManager = new HdHomerunManager(); try { // send url to start streaming - await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false); + await hdHomerunManager.StartStreaming( + remoteAddress, + localAddress, + localPort, + _channelCommands, + _numTuners, + openCancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -103,7 +112,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var taskCompletionSource = new TaskCompletionSource<bool>(); - await StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token); + await StartStreaming( + udpClient, + hdHomerunManager, + remoteAddress, + taskCompletionSource, + LiveStreamCancellationTokenSource.Token).ConfigureAwait(false); //OpenedMediaSource.Protocol = MediaProtocol.File; //OpenedMediaSource.Path = tempFile; @@ -148,50 +162,43 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } - private static void Resolve(TaskCompletionSource<bool> openTaskCompletionSource) - { - Task.Run(() => - { - openTaskCompletionSource.TrySetResult(true); - }); - } - - private const int RtpHeaderBytes = 12; - private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { - var bufferSize = 81920; - - byte[] buffer = new byte[bufferSize]; - int read; - var resolved = false; - - using (var source = _socketFactory.CreateNetworkStream(udpClient, false)) - using (var fileStream = FileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) + byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamDefaults.DefaultCopyToBufferSize); + try { - var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token; - - while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0) + using (var source = _socketFactory.CreateNetworkStream(udpClient, false)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) { - cancellationToken.ThrowIfCancellationRequested(); + var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token; + int read; + var resolved = false; + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); - currentCancellationToken = cancellationToken; + currentCancellationToken = cancellationToken; - read -= RtpHeaderBytes; + read -= RtpHeaderBytes; - if (read > 0) - { - fileStream.Write(buffer, RtpHeaderBytes, read); - } + if (read > 0) + { + await fileStream.WriteAsync(buffer, RtpHeaderBytes, read).ConfigureAwait(false); + } - if (!resolved) - { - resolved = true; - DateOpened = DateTime.UtcNow; - Resolve(openTaskCompletionSource); + if (!resolved) + { + resolved = true; + DateOpened = DateTime.UtcNow; + openTaskCompletionSource.TrySetResult(true); + } } } } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index b4395e2e1..d12c96392 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -16,27 +16,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class LiveStream : ILiveStream { - public MediaSourceInfo OriginalMediaSource { get; set; } - public MediaSourceInfo MediaSource { get; set; } - - public int ConsumerCount { get; set; } - - public string OriginalStreamId { get; set; } - public bool EnableStreamSharing { get; set; } - public string UniqueId { get; } - protected readonly IFileSystem FileSystem; protected readonly IServerApplicationPaths AppPaths; + protected readonly IStreamHelper StreamHelper; protected string TempFilePath; protected readonly ILogger Logger; protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource(); - public string TunerHostId { get; } - - public DateTime DateOpened { get; protected set; } - - public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) + public LiveStream( + MediaSourceInfo mediaSource, + TunerHostInfo tuner, + IFileSystem fileSystem, + ILogger logger, + IServerApplicationPaths appPaths, + IStreamHelper streamHelper) { OriginalMediaSource = mediaSource; FileSystem = fileSystem; @@ -51,11 +45,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } AppPaths = appPaths; + StreamHelper = streamHelper; ConsumerCount = 1; SetTempFilePath("ts"); } + protected virtual int EmptyReadLimit => 1000; + + public MediaSourceInfo OriginalMediaSource { get; set; } + public MediaSourceInfo MediaSource { get; set; } + + public int ConsumerCount { get; set; } + + public string OriginalStreamId { get; set; } + public bool EnableStreamSharing { get; set; } + public string UniqueId { get; } + + public string TunerHostId { get; } + + public DateTime DateOpened { get; protected set; } + protected void SetTempFilePath(string extension) { TempFilePath = Path.Combine(AppPaths.GetTranscodingTempPath(), UniqueId + "." + extension); @@ -71,24 +81,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { EnableStreamSharing = false; - Logger.LogInformation("Closing " + GetType().Name); + Logger.LogInformation("Closing {Type}", GetType().Name); LiveStreamCancellationTokenSource.Cancel(); return Task.CompletedTask; } - protected Stream GetInputStream(string path, bool allowAsyncFileRead) - { - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsyncFileRead) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); - } + protected FileStream GetInputStream(string path, bool allowAsyncFileRead) + => new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + StreamDefaults.DefaultFileStreamBufferSize, + allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); public Task DeleteTempFiles() { @@ -144,8 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; var nextFileInfo = GetNextFile(null); - var nextFile = nextFileInfo.Item1; - var isLastFile = nextFileInfo.Item2; + var nextFile = nextFileInfo.file; + var isLastFile = nextFileInfo.isLastFile; while (!string.IsNullOrEmpty(nextFile)) { @@ -155,8 +162,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts seekFile = false; nextFileInfo = GetNextFile(nextFile); - nextFile = nextFileInfo.Item1; - isLastFile = nextFileInfo.Item2; + nextFile = nextFileInfo.file; + isLastFile = nextFileInfo.isLastFile; } Logger.LogInformation("Live Stream ended."); @@ -180,19 +187,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken) { - using (var inputStream = (FileStream)GetInputStream(path, allowAsync)) + using (var inputStream = GetInputStream(path, allowAsync)) { if (seekFile) { TrySeek(inputStream, -20000); } - await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + inputStream, + stream, + StreamDefaults.DefaultCopyToBufferSize, + emptyReadLimit, + cancellationToken).ConfigureAwait(false); } } - protected virtual int EmptyReadLimit => 1000; - private void TrySeek(FileStream stream, long offset) { if (!stream.CanSeek) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 6c5c80827..a02a9ade4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -28,14 +28,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; - - public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager) + private readonly IStreamHelper _streamHelper; + + public M3UTunerHost( + IServerConfigurationManager config, + IMediaSourceManager mediaSourceManager, + ILogger logger, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + IHttpClient httpClient, + IServerApplicationHost appHost, + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; + _streamHelper = streamHelper; } public override string Type => "m3u"; @@ -103,11 +114,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper); } } - return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths); + return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths, _streamHelper); } public async Task Validate(TunerHostInfo info) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 7de9931c7..c6e894560 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -19,8 +19,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IHttpClient _httpClient; private readonly IServerApplicationHost _appHost; - public SharedHttpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + public SharedHttpStream( + MediaSourceInfo mediaSource, + TunerHostInfo tunerHostInfo, + string originalStreamId, + IFileSystem fileSystem, + IHttpClient httpClient, + ILogger logger, + IServerApplicationPaths appPaths, + IServerApplicationHost appHost, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _httpClient = httpClient; _appHost = appHost; @@ -118,7 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts using (var stream = response.Content) using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) { - await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + stream, + fileStream, + StreamDefaults.DefaultCopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -128,6 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { Logger.LogError(ex, "Error copying live stream."); } + EnableStreamSharing = false; await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); 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; } /// <summary> @@ -503,9 +503,14 @@ namespace MediaBrowser.Api } } + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> public async Task<object> 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 { /// <summary> 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; } - - /// <summary> - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// </summary> - public bool UsesIdForConfigurationPath { get; set; } /// <summary> /// Gets or sets the password. @@ -31,7 +24,6 @@ namespace MediaBrowser.Controller.Entities /// <value>The password.</value> 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 /// <exception cref="ArgumentNullException"></exception> 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 @@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Library IEnumerable<User> Users { get; } /// <summary> + /// Gets the user ids. + /// </summary> + /// <value>The users ids.</value> + IEnumerable<Guid> UsersIds { get; } + + /// <summary> /// Occurs when [user updated]. /// </summary> event EventHandler<GenericEventArgs<User>> UserUpdated; @@ -92,7 +98,7 @@ namespace MediaBrowser.Controller.Library /// <returns>User.</returns> /// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentException"></exception> - Task<User> CreateUser(string name); + User CreateUser(string name); /// <summary> /// Deletes the user. @@ -101,7 +107,7 @@ namespace MediaBrowser.Controller.Library /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">user</exception> /// <exception cref="ArgumentException"></exception> - Task DeleteUser(User user); + void DeleteUser(User user); /// <summary> /// Resets the password. diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs index bef20e74f..1dc29e06e 100644 --- a/MediaBrowser.Model/IO/StreamDefaults.cs +++ b/MediaBrowser.Model/IO/StreamDefaults.cs @@ -13,6 +13,6 @@ namespace MediaBrowser.Model.IO /// <summary> /// The default file stream buffer size /// </summary> - public const int DefaultFileStreamBufferSize = 81920; + public const int DefaultFileStreamBufferSize = 4096; } } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs index c04e98e64..eaebc13e3 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs @@ -50,27 +50,25 @@ namespace MediaBrowser.Providers.TV.TheTVDB var language = item.GetPreferredMetadataLanguage(); if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) { - var episodeTvdbId = episode.GetProviderId(MetadataProviders.Tvdb); - // Process images try { + var episodeInfo = new EpisodeInfo + { + IndexNumber = episode.IndexNumber.Value, + ParentIndexNumber = episode.ParentIndexNumber.Value, + SeriesProviderIds = series.ProviderIds + }; + string episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) { - var episodeInfo = new EpisodeInfo - { - IndexNumber = episode.IndexNumber.Value, - ParentIndexNumber = episode.ParentIndexNumber.Value, - SeriesProviderIds = series.ProviderIds - }; - episodeTvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - episodeInfo.ParentIndexNumber, episodeInfo.IndexNumber, series.GetProviderId(MetadataProviders.Tvdb)); - return imageResult; - } + _logger.LogError( + "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + episodeInfo.ParentIndexNumber, + episodeInfo.IndexNumber, + series.GetProviderId(MetadataProviders.Tvdb)); + return imageResult; } var episodeResult = @@ -86,7 +84,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB } catch (TvDbServerException e) { - _logger.LogError(e, "Failed to retrieve episode images for {TvDbId}", episodeTvdbId); + _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProviders.Tvdb)); } } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index 302d40c6b..e5287048d 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -36,57 +36,33 @@ namespace MediaBrowser.Providers.TV.TheTVDB var list = new List<RemoteSearchResult>(); // The search query must either provide an episode number or date - if (!searchInfo.IndexNumber.HasValue || !searchInfo.PremiereDate.HasValue) + if (!searchInfo.IndexNumber.HasValue + || !searchInfo.PremiereDate.HasValue + || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) { return list; } - if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) + var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); + + if (!metadataResult.HasMetadata) { - try - { - var episodeTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), - out var seriesTvdbId); - episodeTvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); - return list; - } - } + return list; + } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), - searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - var metadataResult = MapEpisodeToResult(searchInfo, episodeResult.Data); + var item = metadataResult.Item; - if (metadataResult.HasMetadata) - { - var item = metadataResult.Item; - - list.Add(new RemoteSearchResult - { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); - } - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", searchInfo.IndexNumber); - } - } + list.Add(new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + }); return list; } @@ -103,36 +79,46 @@ namespace MediaBrowser.Providers.TV.TheTVDB if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) { - var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); - try - { - if (string.IsNullOrEmpty(tvdbId)) - { - tvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - if (string.IsNullOrEmpty(tvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - searchInfo.ParentIndexNumber, searchInfo.IndexNumber, tvdbId); - return result; - } - } + result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); + } + else + { + _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); + } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync( - Convert.ToInt32(tvdbId), searchInfo.MetadataLanguage, - cancellationToken).ConfigureAwait(false); + return result; + } - result = MapEpisodeToResult(searchInfo, episodeResult.Data); - } - catch (TvDbServerException e) + private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var result = new MetadataResult<Episode> + { + QueriedById = true + }; + + string seriesTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); + string episodeTvdbId = null; + try + { + episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + if (string.IsNullOrEmpty(episodeTvdbId)) { - _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", tvdbId); + _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); + return result; } + + var episodeResult = await _tvDbClientManager.GetEpisodesAsync( + Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage, + cancellationToken).ConfigureAwait(false); + + result = MapEpisodeToResult(searchInfo, episodeResult.Data); } - else + catch (TvDbServerException e) { - _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); + _logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId); } return result; @@ -193,24 +179,54 @@ namespace MediaBrowser.Providers.TV.TheTVDB }); } - foreach (var person in episode.GuestStars) + // GuestStars is a weird list of names and roles + // Example: + // 1: Some Actor (Role1 + // 2: Role2 + // 3: Role3) + // 4: Another Actor (Role1 + // ... + for (var i = 0; i < episode.GuestStars.Length; ++i) { - var index = person.IndexOf('('); - string role = null; - var name = person; + var currentActor = episode.GuestStars[i]; + var roleStartIndex = currentActor.IndexOf('('); - if (index != -1) + if (roleStartIndex == -1) { - role = person.Substring(index + 1).Trim().TrimEnd(')'); + result.AddPerson(new PersonInfo + { + Type = PersonType.GuestStar, + Name = currentActor, + Role = string.Empty + }); + continue; + } + + var roles = new List<string> {currentActor.Substring(roleStartIndex + 1)}; + + // Fetch all roles + for (var j = i + 1; j < episode.GuestStars.Length; ++j) + { + var currentRole = episode.GuestStars[j]; + var roleEndIndex = currentRole.IndexOf(')'); + + if (roleEndIndex == -1) + { + roles.Add(currentRole); + continue; + } - name = person.Substring(0, index).Trim(); + roles.Add(currentRole.TrimEnd(')')); + // Update the outer index (keep in mind it adds 1 after the iteration) + i = j; + break; } result.AddPerson(new PersonInfo { Type = PersonType.GuestStar, - Name = name, - Role = role + Name = currentActor.Substring(0, roleStartIndex).Trim(), + Role = string.Join(", ", roles) }); } |
