From 49d9649b8eb7e9edc889c10a679e980486ee0018 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 30 Jan 2019 23:35:17 -0800 Subject: [PATCH 0001/6208] added submodule dir to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aef6662727..3bea16e5d2 100644 --- a/.gitignore +++ b/.gitignore @@ -264,3 +264,4 @@ deployment/**/pkg-dist-tmp/ deployment/collect-dist/ jellyfin_version.ini +MediaBrowser.WebDashboard/jellyfin-web From 4519ce26e2250cb233836296d292ddb7b3cf6346 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Thu, 31 Jan 2019 00:24:53 -0800 Subject: [PATCH 0002/6208] Upgrade crypto provider, retarget better framework --- .../Cryptography/CryptographyProvider.cs | 171 ++++++++++++++---- .../Emby.Server.Implementations.csproj | 2 +- .../Library/UserManager.cs | 31 ++-- .../Cryptography/ICryptoProvider.cs | 8 + .../Cryptography/PasswordHash.cs | 93 ++++++++++ 5 files changed, 248 insertions(+), 57 deletions(-) create mode 100644 MediaBrowser.Model/Cryptography/PasswordHash.cs diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 09fdbc856d..ca6ae2bb2d 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,40 +1,131 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Cryptography -{ - public class CryptographyProvider : ICryptoProvider - { - public Guid GetMD5(string str) - { - return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - } - - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Cryptography +{ + public class CryptographyProvider : ICryptoProvider + { + private List SupportedHashMethods = new List(); + private string DefaultHashMethod = "SHA256"; + private RandomNumberGenerator rng; + private int defaultiterations = 1000; + public CryptographyProvider() + { + //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 + //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one + SupportedHashMethods = new List + { + "MD5" + ,"System.Security.Cryptography.MD5" + ,"SHA" + ,"SHA1" + ,"System.Security.Cryptography.SHA1" + ,"SHA256" + ,"SHA-256" + ,"System.Security.Cryptography.SHA256" + ,"SHA384" + ,"SHA-384" + ,"System.Security.Cryptography.SHA384" + ,"SHA512" + ,"SHA-512" + ,"System.Security.Cryptography.SHA512" + }; + rng = RandomNumberGenerator.Create(); + } + + public Guid GetMD5(string str) + { + return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); + } + + public byte[] ComputeSHA1(byte[] bytes) + { + using (var provider = SHA1.Create()) + { + return provider.ComputeHash(bytes); + } + } + + public byte[] ComputeMD5(Stream str) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(str); + } + } + + public byte[] ComputeMD5(byte[] bytes) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(bytes); + } + } + + public IEnumerable GetSupportedHashMethods() + { + return SupportedHashMethods; + } + + private byte[] PBKDF2(string method, byte[] bytes, byte[] salt) + { + using (var r = new Rfc2898DeriveBytes(bytes, salt, defaultiterations, new HashAlgorithmName(method))) + { + return r.GetBytes(32); + } + } + + public byte[] ComputeHash(string HashMethod, byte[] bytes) + { + return ComputeHash(HashMethod, bytes, new byte[0]); + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes) + { + return ComputeHash(DefaultHashMethod, bytes); + } + + public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) + { + if (SupportedHashMethods.Contains(HashMethod)) + { + if (salt.Length == 0) + { + using (var h = HashAlgorithm.Create(HashMethod)) + { + return h.ComputeHash(bytes); + } + } + else + { + return PBKDF2(HashMethod, bytes, salt); + } + } + else + { + throw new CryptographicException(String.Format("Requested hash method is not supported: {0}", HashMethod)); + } + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) + { + return PBKDF2(DefaultHashMethod, bytes, salt); + } + + public byte[] ComputeHash(PasswordHash hash) + { + return ComputeHash(hash.Id, hash.HashBytes, hash.SaltBytes); + } + + public byte[] GenerateSalt() + { + byte[] salt = new byte[8]; + rng.GetBytes(salt); + return salt; + } + } +} diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3aa617b021..df7963b023 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -35,7 +35,7 @@ - netstandard2.0 + netcoreapp2.1 false diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 05fce4542f..70639dad5f 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Events; @@ -220,22 +221,20 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) - { - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - foreach (var currentChar in username) - { - if (!IsValidUsernameCharacter(currentChar)) - { - return false; - } - } - return true; - } - - private static bool IsValidUsernameCharacter(char i) - { - return !char.Equals(i, '<') && !char.Equals(i, '>'); + public bool IsValidUsername(string username) + { + //The old way was dumb, we should make it less dumb, lets do so. + //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 + string UserNameRegex = "^[\\w-'._@]*$"; + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(username, UserNameRegex); + } + + private static bool IsValidUsernameCharacter(char i) + { + string UserNameRegex = "^[\\w-'._@]*$"; + return Regex.IsMatch(i.ToString(), UserNameRegex); } public string MakeValidUsername(string username) diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index b027d2ad0b..ec7e57fec5 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography { @@ -9,5 +10,12 @@ namespace MediaBrowser.Model.Cryptography byte[] ComputeMD5(Stream str); byte[] ComputeMD5(byte[] bytes); 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(); } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs new file mode 100644 index 0000000000..d37220ab2f --- /dev/null +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +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 + //$[$=(,=)*][$[$]] + + public string Id; + public Dictionary Parameters = new Dictionary(); + public string Salt; + public byte[] SaltBytes; + public string Hash; + public byte[] HashBytes; + public PasswordHash(string StorageString) + { + string[] a = StorageString.Split('$'); + Id = a[1]; + if (a[2].Contains("=")) + { + foreach (string paramset in (a[2].Split(','))) + { + if (!String.IsNullOrEmpty(paramset)) + { + string[] fields = paramset.Split('='); + Parameters.Add(fields[0], fields[1]); + } + } + if (a.Length == 4) + { + Salt = a[2]; + SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + Hash = a[3]; + HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + } + else + { + Salt = string.Empty; + Hash = a[3]; + HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + } + } + else + { + if (a.Length == 4) + { + Salt = a[2]; + SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + Hash = a[3]; + HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + } + else + { + Salt = string.Empty; + Hash = a[2]; + HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + } + + } + + } + + public PasswordHash(ICryptoProvider cryptoProvider2) + { + Id = "SHA256"; + SaltBytes = cryptoProvider2.GenerateSalt(); + Salt = Convert.ToBase64String(SaltBytes); + } + private string SerializeParameters() + { + string ReturnString = String.Empty; + foreach (var KVP in Parameters) + { + ReturnString += String.Format(",{0}={1}", KVP.Key, KVP.Value); + } + if (ReturnString[0] == ',') + { + ReturnString = ReturnString.Remove(0, 1); + } + return ReturnString; + } + + public override string ToString() + { + return String.Format("${0}${1}${2}${3}", Id, SerializeParameters(), Salt, Hash); + } + } + +} From 05bbf71b6d97614888efe103f763753e4487cc2c Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 12 Feb 2019 02:16:03 -0800 Subject: [PATCH 0003/6208] sha256 with salt auth and sha1 interop --- .../Cryptography/CryptographyProvider.cs | 2 +- .../Library/DefaultAuthenticationProvider.cs | 169 +- .../Library/UserManager.cs | 2416 ++++++++--------- .../Cryptography/ICryptoProvider.cs | 29 +- .../Cryptography/PasswordHash.cs | 37 +- 5 files changed, 1389 insertions(+), 1264 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index ca6ae2bb2d..4f2bc1b030 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -10,7 +10,7 @@ namespace Emby.Server.Implementations.Cryptography public class CryptographyProvider : ICryptoProvider { private List SupportedHashMethods = new List(); - private string DefaultHashMethod = "SHA256"; + public string DefaultHashMethod => "SHA256"; private RandomNumberGenerator rng; private int defaultiterations = 1000; public CryptographyProvider() diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 4013ac0c80..92346c65aa 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text; using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; @@ -19,31 +20,110 @@ namespace Emby.Server.Implementations.Library 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 public Task Authenticate(string username, string password) { throw new NotImplementedException(); } - public Task Authenticate(string username, string password, User resolvedUser) - { - if (resolvedUser == null) - { - throw new Exception("Invalid username or password"); - } - var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - - if (!success) - { - throw new Exception("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); + //This is the verson that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, User resolvedUser) + { + ConvertPasswordFormat(resolvedUser); + byte[] passwordbytes = Encoding.UTF8.GetBytes(password); + bool success = false; + if (resolvedUser == null) + { + success = false; + throw new Exception("Invalid username or password"); + } + if (!resolvedUser.Password.Contains("$")) + { + ConvertPasswordFormat(resolvedUser); + } + PasswordHash ReadyHash = new PasswordHash(resolvedUser.Password); + byte[] CalculatedHash; + string CalculatedHashString; + if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == ReadyHash.Id)) + { + if (String.IsNullOrEmpty(ReadyHash.Salt)) + { + CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + } + else + { + CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes, ReadyHash.SaltBytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + } + if (CalculatedHashString == ReadyHash.Hash) + { + success = true; + //throw new Exception("Invalid username or password"); + } + } + else + { + success = false; + throw new Exception(String.Format("Requested crypto method not available in provider: {0}", ReadyHash.Id)); + } + + //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); } + //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)) + { + if (!user.Password.Contains("$")) + { + string hash = user.Password; + user.Password = String.Format("$SHA1${0}", hash); + } + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = String.Format("$SHA1${0}", hash); + } + } + } + + // OLD VERSION //public Task Authenticate(string username, string password, User resolvedUser) + // OLD VERSION //{ + // OLD VERSION // if (resolvedUser == null) + // OLD VERSION // { + // OLD VERSION // throw new Exception("Invalid username or password"); + // OLD VERSION // } + // OLD VERSION // + // OLD VERSION // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + // OLD VERSION // + // OLD VERSION // if (!success) + // OLD VERSION // { + // OLD VERSION // throw new Exception("Invalid username or password"); + // OLD VERSION // } + // OLD VERSION // + // OLD VERSION // return Task.FromResult(new ProviderAuthenticationResult + // OLD VERSION // { + // OLD VERSION // Username = username + // OLD VERSION // }); + // OLD VERSION //} + public Task HasPassword(User user) { var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); @@ -57,19 +137,26 @@ namespace Emby.Server.Implementations.Library public Task ChangePassword(User user, string newPassword) { - string newPasswordHash = null; - - if (newPassword != null) + //string newPasswordHash = null; + ConvertPasswordFormat(user); + PasswordHash passwordHash = new PasswordHash(user.Password); + if(passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { - newPasswordHash = GetHashedString(user, newPassword); + passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + passwordHash.Salt = BitConverter.ToString(passwordHash.SaltBytes).Replace("-",""); + passwordHash.Id = _cryptographyProvider.DefaultHashMethod; + passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); + }else if (newPassword != null) + { + passwordHash.Hash = GetHashedString(user, newPassword); } - if (string.IsNullOrWhiteSpace(newPasswordHash)) + if (string.IsNullOrWhiteSpace(passwordHash.Hash)) { - throw new ArgumentNullException(nameof(newPasswordHash)); + throw new ArgumentNullException(nameof(passwordHash.Hash)); } - user.Password = newPasswordHash; + user.Password = passwordHash.ToString(); return Task.CompletedTask; } @@ -86,19 +173,39 @@ namespace Emby.Server.Implementations.Library return GetHashedString(user, string.Empty); } + public string GetHashedStringChangeAuth(string NewPassword, PasswordHash passwordHash) + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(NewPassword), passwordHash.SaltBytes)).Replace("-", string.Empty); + } + /// /// Gets the hashed string. /// - public string GetHashedString(User user, string str) - { - var salt = user.Salt; - if (salt != null) + public string GetHashedString(User user, string str) + { + //This is legacy. Deprecated in the auth method. + //return BitConverter.ToString(_cryptoProvider2.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + PasswordHash passwordHash; + if (String.IsNullOrEmpty(user.Password)) + { + passwordHash = new PasswordHash(_cryptographyProvider); + } + else { - // return BCrypt.HashPassword(str, salt); + ConvertPasswordFormat(user); + passwordHash = new PasswordHash(user.Password); + } + if (passwordHash.SaltBytes != null) + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str), passwordHash.SaltBytes)).Replace("-",string.Empty); + } + else + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + //throw new Exception("User does not have a hash, this should not be possible"); } - // legacy - return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 40eda52c6a..a139c4e736 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,222 +1,222 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -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; -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; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Library -{ - /// - /// Class UserManager - /// - 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; } - - /// - /// Gets the active user repository - /// - /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - - private readonly INetworkManager _networkManager; - - private readonly Func _imageProcessorFactory; - private readonly Func _dtoServiceFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - private readonly ICryptoProvider _cryptographyProvider; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - - public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, - IUserRepository userRepository, - IXmlSerializer xmlSerializer, - INetworkManager networkManager, - Func imageProcessorFactory, - Func dtoServiceFactory, - IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - IFileSystem fileSystem, - ICryptoProvider cryptographyProvider) - { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _cryptographyProvider = cryptographyProvider; - ConfigurationManager = configurationManager; - _users = Array.Empty(); - - DeletePinFile(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(i => i.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = GetAuthenticationProviderId(i) - }) - .ToArray(); - } - - public void AddParts(IEnumerable authenticationProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.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 - /// - /// The id. - /// User. - /// - public User GetUserById(Guid id) - { - if (id.Equals(Guid.Empty)) - { - throw new ArgumentNullException(nameof(id)); - } - - return Users.FirstOrDefault(u => u.Id == id); - } - - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. - public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } - - public User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - public void Initialize() - { - _users = LoadUsers(); - - var users = Users.ToList(); - - // If there are no local users with admin rights, make them all admins - if (!users.Any(i => i.Policy.IsAdministrator)) - { - foreach (var user in users) - { - user.Policy.IsAdministrator = true; - UpdateUserPolicy(user, user.Policy, false); - } - } - } - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +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; +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; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Class UserManager + /// + 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; } + + /// + /// Gets the active user repository + /// + /// The user repository. + private IUserRepository UserRepository { get; set; } + public event EventHandler> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func _imageProcessorFactory; + private readonly Func _dtoServiceFactory; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + private readonly ICryptoProvider _cryptographyProvider; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + + public UserManager( + ILoggerFactory loggerFactory, + IServerConfigurationManager configurationManager, + IUserRepository userRepository, + IXmlSerializer xmlSerializer, + INetworkManager networkManager, + Func imageProcessorFactory, + Func dtoServiceFactory, + IServerApplicationHost appHost, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + ICryptoProvider cryptographyProvider) + { + _logger = loggerFactory.CreateLogger(nameof(UserManager)); + UserRepository = userRepository; + _xmlSerializer = xmlSerializer; + _networkManager = networkManager; + _imageProcessorFactory = imageProcessorFactory; + _dtoServiceFactory = dtoServiceFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _cryptographyProvider = cryptographyProvider; + ConfigurationManager = configurationManager; + _users = Array.Empty(); + + DeletePinFile(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.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 + /// + /// The id. + /// User. + /// + public User GetUserById(Guid id) + { + if (id.Equals(Guid.Empty)) + { + throw new ArgumentNullException(nameof(id)); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// + /// Gets the user by identifier. + /// + /// The identifier. + /// User. + public User GetUserById(string id) + { + return GetUserById(new Guid(id)); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public void Initialize() + { + _users = LoadUsers(); + + var users = Users.ToList(); + + // If there are no local users with admin rights, make them all admins + if (!users.Any(i => i.Policy.IsAdministrator)) + { + foreach (var user in users) + { + user.Policy.IsAdministrator = true; + UpdateUserPolicy(user, user.Policy, false); + } + } + } + public bool IsValidUsername(string username) { //The old way was dumb, we should make it less dumb, lets do so. @@ -231,992 +231,992 @@ namespace Emby.Server.Implementations.Library { string UserNameRegex = "^[\\w-'._@]*$"; return Regex.IsMatch(i.ToString(), UserNameRegex); - } - - public string MakeValidUsername(string username) - { - if (IsValidUsername(username)) - { - return username; - } - - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - var builder = new StringBuilder(); - - foreach (var c in username) - { - if (IsValidUsernameCharacter(c)) - { - builder.Append(c); - } - } - return builder.ToString(); - } - - public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - - var success = false; - IAuthenticationProvider authenticationProvider = null; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - } - else - { - // user is null - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) - { - user = await CreateUser(username).ConfigureAwait(false); - - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) - { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = GetAuthenticationProviderId(authenticationProvider); - - if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.Policy.AuthenticationProviderId = providerId; - UpdateUserPolicy(user, user.Policy, true); - } - } - - if (user == null) - { - throw new SecurityException("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)); - } - - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - UpdateInvalidLoginAttemptCount(user, 0); - } - else - { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); - } - - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - - return success ? user : null; - } - - private static string GetAuthenticationProviderId(IAuthenticationProvider provider) - { - return provider.GetType().FullName; - } - - private IAuthenticationProvider GetAuthenticationProvider(User user) - { - return GetAuthenticationProviders(user).First(); - } - - private IAuthenticationProvider[] GetAuthenticationProviders(User user) - { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - if (providers.Length == 0) - { - providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; - } - - return providers; - } - - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) - { - try - { - var requiresResolvedUser = provider as IRequiresResolvedUser; - if (requiresResolvedUser != null) - { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - await provider.Authenticate(username, password).ConfigureAwait(false); - } - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - - return false; - } - } - - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - if (password != null && user != null) - { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } - - if (password == null) - { - // legacy - success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) - { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - - if (success) - { - authenticationProvider = provider; - break; - } - } - } - - if (user != null) - { - 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); - } - } - } - - return new Tuple(authenticationProvider, success); - } - - private void UpdateInvalidLoginAttemptCount(User user, int newValue) - { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - var maxCount = user.Policy.IsAdministrator ? 3 : 5; - - var fireLockout = false; - - if (newValue >= maxCount) - { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); - user.Policy.IsDisabled = true; - - fireLockout = true; - } - - UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } - } - - private string GetLocalPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) - : user.EasyPassword; - } - - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - - /// - /// Loads the users from the repository - /// - /// IEnumerable{User}. - private User[] LoadUsers() - { - var users = UserRepository.RetrieveAllUsers(); - - // There always has to be at least one user. - if (users.Count == 0) - { - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - var name = MakeValidUsername(defaultName); - - var user = InstantiateNewUser(name); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - users.Add(user); - - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - } - - return users.ToArray(); - } - - public UserDto GetUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); - - var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? - hasConfiguredEasyPassword : - hasConfiguredPassword; - - var dto = new UserDto - { - Id = user.Id, - Name = user.Name, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword, - HasConfiguredEasyPassword = hasConfiguredEasyPassword, - LastActivityDate = user.LastActivityDate, - LastLoginDate = user.LastLoginDate, - Configuration = user.Configuration, - ServerId = _appHost.SystemId, - Policy = user.Policy - }; - - if (!hasPassword && Users.Count() == 1) - { - dto.EnableAutoLogin = true; - } - - var image = user.GetImageInfo(ImageType.Primary, 0); - - if (image != null) - { - dto.PrimaryImageTag = GetImageCacheTag(user, image); - - try - { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); - } - catch (Exception ex) - { - // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); - } - } - - return dto; - } - - public UserDto GetOfflineUserDto(User user) - { - var dto = GetUserDto(user); - - dto.ServerName = _appHost.FriendlyName; - - return dto; - } - - private string GetImageCacheTag(BaseItem item, ItemImageInfo image) - { - try - { - return _imageProcessorFactory().GetImageCacheTag(item, image); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); - return null; - } - } - - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - public async Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - foreach (var user in Users) - { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Renames the user. - /// - /// The user. - /// The new name. - /// Task. - /// user - /// - public async Task RenameUser(User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); - } - - if (user.Name.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - await user.Rename(newName); - - OnUserUpdated(user); - } - - /// - /// Updates the user. - /// - /// The user. - /// user - /// - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) - { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); - } - - user.DateModified = DateTime.UtcNow; - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.UpdateUser(user); - - OnUserUpdated(user); - } - - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - - /// - /// Creates the user. - /// - /// The name. - /// User. - /// name - /// - public async Task CreateUser(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!IsValidUsername(name)) - { - throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); - } - - if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - 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 list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - - return user; - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - /// - public async Task DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) - { - 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)); - } - - if (allUsers.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)); - } - - if (user.Policy.IsAdministrator && allUsers.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)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - 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); - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Resets the password by clearing it. - /// - /// Task. - public Task ResetPassword(User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (newPassword != null) - { - newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - /// - /// Instantiates the new user. - /// - /// The name. - /// User. - private static User InstantiateNewUser(string name) - { - return new User - { - Name = name, - Id = Guid.NewGuid(), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - //Salt = BCrypt.GenerateSalt() - }; - } - - private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); - - private string _lastPin; - private PasswordPinCreationResult _lastPasswordPinCreationResult; - private int _pinAttempts; - - private async Task CreatePasswordResetPin() - { - var num = new Random().Next(1, 9999); - - var path = PasswordResetFile; - - var pin = num.ToString("0000", CultureInfo.InvariantCulture); - _lastPin = pin; - - var time = TimeSpan.FromMinutes(5); - var expiration = DateTime.UtcNow.Add(time); - - var text = new StringBuilder(); - - var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; - - text.AppendLine("Use your web browser to visit:"); - text.AppendLine(string.Empty); - text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); - text.AppendLine(string.Empty); - text.AppendLine("Enter the following pin code:"); - text.AppendLine(string.Empty); - text.AppendLine(pin); - text.AppendLine(string.Empty); - - var localExpirationTime = expiration.ToLocalTime(); - // Tuesday, 22 August 2006 06:30 AM - text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); - - File.WriteAllText(path, text.ToString(), Encoding.UTF8); - - var result = new PasswordPinCreationResult - { - PinFile = path, - ExpirationDate = expiration - }; - - _lastPasswordPinCreationResult = result; - _pinAttempts = 0; - - return result; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - DeletePinFile(); - - var user = string.IsNullOrWhiteSpace(enteredUsername) ? - null : - GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - string pinFile = null; - DateTime? expirationDate = null; - - if (user != null && !user.Policy.IsAdministrator) - { - action = ForgotPasswordAction.ContactAdmin; - } - else - { - if (isInNetwork) - { - action = ForgotPasswordAction.PinCode; - } - - var result = await CreatePasswordResetPin().ConfigureAwait(false); - pinFile = result.PinFile; - expirationDate = result.ExpirationDate; - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = pinFile, - PinExpirationDate = expirationDate - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - DeletePinFile(); - - var usersReset = new List(); - - var valid = !string.IsNullOrWhiteSpace(_lastPin) && - string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && - _lastPasswordPinCreationResult != null && - _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; - - if (valid) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - - foreach (var user in Users) - { - await ResetPassword(user).ConfigureAwait(false); - - if (user.Policy.IsDisabled) - { - user.Policy.IsDisabled = false; - UpdateUserPolicy(user, user.Policy, true); - } - usersReset.Add(user.Name); - } - } - else - { - _pinAttempts++; - if (_pinAttempts >= 3) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - } - } - - return new PinRedeemResult - { - Success = valid, - UsersReset = usersReset.ToArray() - }; - } - - private void DeletePinFile() - { - try - { - _fileSystem.DeleteFile(PasswordResetFile); - } - catch - { - - } - } - - class PasswordPinCreationResult - { - public string PinFile { get; set; } - public DateTime ExpirationDate { get; set; } - } - - public UserPolicy GetUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - if (!File.Exists(path)) - { - return GetDefaultPolicy(user); - } - - try - { - lock (_policySyncLock) - { - return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); - } - } - catch (IOException) - { - return GetDefaultPolicy(user); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return GetDefaultPolicy(user); - } - } - - private static UserPolicy GetDefaultPolicy(User user) - { - return new UserPolicy - { - EnableContentDownloading = true, - EnableSyncTranscoding = true - }; - } - - private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) - { - var user = GetUserById(userId); - UpdateUserPolicy(user, userPolicy, true); - } - - private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) - { - // The xml serializer will output differently if the type is not exact - if (userPolicy.GetType() != typeof(UserPolicy)) - { - var json = _jsonSerializer.SerializeToString(userPolicy); - userPolicy = _jsonSerializer.DeserializeFromString(json); - } - - var path = GetPolicyFilePath(user); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_policySyncLock) - { - _xmlSerializer.SerializeToFile(userPolicy, path); - user.Policy = userPolicy; - } - - if (fireEvent) - { - UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - - private void DeleteUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - try - { - lock (_policySyncLock) - { - _fileSystem.DeleteFile(path); - } - } - catch (IOException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting policy file"); - } - } - - private static string GetPolicyFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); - } - - private static string GetConfigurationFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); - } - - public UserConfiguration GetUserConfiguration(User user) - { - var path = GetConfigurationFilePath(user); - - if (!File.Exists(path)) - { - return new UserConfiguration(); - } - - try - { - lock (_configSyncLock) - { - return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); - } - } - catch (IOException) - { - return new UserConfiguration(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return new UserConfiguration(); - } - } - - private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - UpdateConfiguration(user, config); - } - - public void UpdateConfiguration(User user, UserConfiguration config) - { - UpdateConfiguration(user, config, true); - } - - private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) - { - var path = GetConfigurationFilePath(user); - - // The xml serializer will output differently if the type is not exact - if (config.GetType() != typeof(UserConfiguration)) - { - var json = _jsonSerializer.SerializeToString(config); - config = _jsonSerializer.DeserializeFromString(json); - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - } - - public class DeviceAccessEntryPoint : IServerEntryPoint - { - private IUserManager _userManager; - private IAuthenticationRepository _authRepo; - private IDeviceManager _deviceManager; - private ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - - return Task.CompletedTask; - } - - private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.Policy.EnableAllDevices) - { - UpdateDeviceAccess(user); - } - } - - private void UpdateDeviceAccess(User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - - public void Dispose() - { - - } - } -} + } + + public string MakeValidUsername(string username) + { + if (IsValidUsername(username)) + { + return username; + } + + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + var builder = new StringBuilder(); + + foreach (var c in username) + { + if (IsValidUsernameCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + var success = false; + IAuthenticationProvider authenticationProvider = null; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } + + if (user == null) + { + throw new SecurityException("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)); + } + + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + UpdateUser(user); + } + UpdateInvalidLoginAttemptCount(user, 0); + } + else + { + UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + } + + _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); + + return success ? user : null; + } + + private static string GetAuthenticationProviderId(IAuthenticationProvider provider) + { + return provider.GetType().FullName; + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + if (providers.Length == 0) + { + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; + } + + return providers; + } + + private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try + { + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) + { + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + } + else + { + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + + return false; + } + } + + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } + } + } + + if (user != null) + { + 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); + } + } + } + + return new Tuple(authenticationProvider, success); + } + + private void UpdateInvalidLoginAttemptCount(User user, int newValue) + { + if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) + { + return; + } + + user.Policy.InvalidLoginAttemptCount = newValue; + + var maxCount = user.Policy.IsAdministrator ? 3 : 5; + + var fireLockout = false; + + if (newValue >= maxCount) + { + _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); + user.Policy.IsDisabled = true; + + fireLockout = true; + } + + UpdateUserPolicy(user, user.Policy, false); + + if (fireLockout) + { + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + } + } + + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + : user.EasyPassword; + } + + private bool IsPasswordEmpty(User user, string passwordHash) + { + return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + private User[] LoadUsers() + { + var users = UserRepository.RetrieveAllUsers(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyJellyfinUser"; + } + var name = MakeValidUsername(defaultName); + + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + users.Add(user); + + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + UpdateUserPolicy(user, user.Policy, false); + } + + return users.ToArray(); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); + + var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + hasConfiguredEasyPassword : + hasConfiguredPassword; + + var dto = new UserDto + { + Id = user.Id, + Name = user.Name, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword, + HasConfiguredEasyPassword = hasConfiguredEasyPassword, + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration, + ServerId = _appHost.SystemId, + Policy = user.Policy + }; + + if (!hasPassword && Users.Count() == 1) + { + dto.EnableAutoLogin = true; + } + + var image = user.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + dto.PrimaryImageTag = GetImageCacheTag(user, image); + + try + { + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); + } + } + + return dto; + } + + public UserDto GetOfflineUserDto(User user) + { + var dto = GetUserDto(user); + + dto.ServerName = _appHost.FriendlyName; + + return dto; + } + + private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + { + try + { + return _imageProcessorFactory().GetImageCacheTag(item, image); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); + return null; + } + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// Task. + public async Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + foreach (var user in Users) + { + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.UpdateUser(user); + + OnUserUpdated(user); + } + + public event EventHandler> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task CreateUser(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + 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 list = Users.ToList(); + list.Add(user); + _users = list.ToArray(); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var allUsers = Users.ToList(); + + if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + { + 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)); + } + + if (allUsers.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)); + } + + if (user.Policy.IsAdministrator && allUsers.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)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + 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); + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Resets the password by clearing it. + /// + /// Task. + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (newPassword != null) + { + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + private static User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = Guid.NewGuid(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + UsesIdForConfigurationPath = true, + //Salt = BCrypt.GenerateSalt() + }; + } + + private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private async Task CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + + var localExpirationTime = expiration.ToLocalTime(); + // Tuesday, 22 August 2006 06:30 AM + text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); + + File.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + DeletePinFile(); + + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = await CreatePasswordResetPin().ConfigureAwait(false); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + foreach (var user in Users) + { + await ResetPassword(user).ConfigureAwait(false); + + if (user.Policy.IsDisabled) + { + user.Policy.IsDisabled = false; + UpdateUserPolicy(user, user.Policy, true); + } + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + _fileSystem.DeleteFile(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + if (!File.Exists(path)) + { + return GetDefaultPolicy(user); + } + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (IOException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return GetDefaultPolicy(user); + } + } + + private static UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableContentDownloading = true, + EnableSyncTranscoding = true + }; + } + + private readonly object _policySyncLock = new object(); + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + UpdateUserPolicy(user, userPolicy, true); + } + + private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) + { + // The xml serializer will output differently if the type is not exact + if (userPolicy.GetType() != typeof(UserPolicy)) + { + var json = _jsonSerializer.SerializeToString(userPolicy); + userPolicy = _jsonSerializer.DeserializeFromString(json); + } + + var path = GetPolicyFilePath(user); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + if (fireEvent) + { + UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + try + { + lock (_policySyncLock) + { + _fileSystem.DeleteFile(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting policy file"); + } + } + + private static string GetPolicyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private static string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + if (!File.Exists(path)) + { + return new UserConfiguration(); + } + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (IOException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + UpdateConfiguration(user, config); + } + + public void UpdateConfiguration(User user, UserConfiguration config) + { + UpdateConfiguration(user, config, true); + } + + private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) + { + var path = GetConfigurationFilePath(user); + + // The xml serializer will output differently if the type is not exact + if (config.GetType() != typeof(UserConfiguration)) + { + var json = _jsonSerializer.SerializeToString(config); + config = _jsonSerializer.DeserializeFromString(json); + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + } + + public class DeviceAccessEntryPoint : IServerEntryPoint + { + private IUserManager _userManager; + private IAuthenticationRepository _authRepo; + private IDeviceManager _deviceManager; + private ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + + return Task.CompletedTask; + } + + private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.Policy.EnableAllDevices) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + + public void Dispose() + { + + } + } +} diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index ec7e57fec5..8accc696e3 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,15 +1,15 @@ -using System; -using System.IO; -using System.Collections.Generic; - -namespace MediaBrowser.Model.Cryptography -{ - public interface ICryptoProvider - { - Guid GetMD5(string str); - byte[] ComputeMD5(Stream str); - byte[] ComputeMD5(byte[] bytes); - byte[] ComputeSHA1(byte[] bytes); +using System; +using System.IO; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Cryptography +{ + public interface ICryptoProvider + { + Guid GetMD5(string str); + byte[] ComputeMD5(Stream str); + byte[] ComputeMD5(byte[] bytes); + byte[] ComputeSHA1(byte[] bytes); IEnumerable GetSupportedHashMethods(); byte[] ComputeHash(string HashMethod, byte[] bytes); byte[] ComputeHashWithDefaultMethod(byte[] bytes); @@ -17,5 +17,6 @@ namespace MediaBrowser.Model.Cryptography byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); byte[] ComputeHash(PasswordHash hash); byte[] GenerateSalt(); - } -} + string DefaultHashMethod { get; } + } +} diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index d37220ab2f..524484b107 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -33,15 +33,15 @@ namespace MediaBrowser.Model.Cryptography if (a.Length == 4) { Salt = a[2]; - SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + SaltBytes = FromByteString(Salt); Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } else { Salt = string.Empty; Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } } else @@ -49,15 +49,15 @@ namespace MediaBrowser.Model.Cryptography if (a.Length == 4) { Salt = a[2]; - SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + SaltBytes = FromByteString(Salt); Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } else { Salt = string.Empty; Hash = a[2]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } } @@ -68,7 +68,17 @@ namespace MediaBrowser.Model.Cryptography { Id = "SHA256"; SaltBytes = cryptoProvider2.GenerateSalt(); - Salt = Convert.ToBase64String(SaltBytes); + Salt = BitConverter.ToString(SaltBytes).Replace("-", ""); + } + + private byte[] FromByteString(string ByteString) + { + List Bytes = new List(); + for (int i = 0; i < ByteString.Length; i += 2) + { + Bytes.Add(Convert.ToByte(ByteString.Substring(i, 2),16)); + } + return Bytes.ToArray(); } private string SerializeParameters() { @@ -77,7 +87,7 @@ namespace MediaBrowser.Model.Cryptography { ReturnString += String.Format(",{0}={1}", KVP.Key, KVP.Value); } - if (ReturnString[0] == ',') + if ((!string.IsNullOrEmpty(ReturnString)) && ReturnString[0] == ',') { ReturnString = ReturnString.Remove(0, 1); } @@ -85,8 +95,15 @@ namespace MediaBrowser.Model.Cryptography } public override string ToString() - { - return String.Format("${0}${1}${2}${3}", Id, SerializeParameters(), Salt, Hash); + { + string OutString = "$"; + OutString += Id; + if (!string.IsNullOrEmpty(SerializeParameters())) + OutString += $"${SerializeParameters()}"; + if (!string.IsNullOrEmpty(Salt)) + OutString += $"${Salt}"; + OutString += $"${Hash}"; + return OutString; } } From 1dc5a624a7b735fe872110a2a56529560381abf0 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 12 Feb 2019 22:24:05 -0800 Subject: [PATCH 0004/6208] fixed gitignore fail --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3bea16e5d2..e48ab1e798 100644 --- a/.gitignore +++ b/.gitignore @@ -263,5 +263,4 @@ deployment/**/pkg-dist/ deployment/**/pkg-dist-tmp/ deployment/collect-dist/ -jellyfin_version.ini -MediaBrowser.WebDashboard/jellyfin-web +jellyfin_version.ini \ No newline at end of file From 1ffd443d5aaec408170eaec31923a1cbbe1bb929 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 12 Feb 2019 22:30:26 -0800 Subject: [PATCH 0005/6208] fixed nul user check to be first per justaman --- .../Library/DefaultAuthenticationProvider.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 92346c65aa..255fd82527 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -32,15 +32,16 @@ namespace Emby.Server.Implementations.Library //This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) - { - ConvertPasswordFormat(resolvedUser); - byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - bool success = false; + { + bool success = false; if (resolvedUser == null) { success = false; throw new Exception("Invalid username or password"); } + ConvertPasswordFormat(resolvedUser); + byte[] passwordbytes = Encoding.UTF8.GetBytes(password); + if (!resolvedUser.Password.Contains("$")) { ConvertPasswordFormat(resolvedUser); From 77602aff889e605f8178ecf95592c0d75102e59f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 13 Feb 2019 00:33:00 -0800 Subject: [PATCH 0006/6208] Minor fixes re:PR870, added null checks from PR876 --- .../Cryptography/CryptographyProvider.cs | 38 ++++++---- .../Data/SqliteUserRepository.cs | 32 ++++++++ .../Library/DefaultAuthenticationProvider.cs | 71 ++++++------------ .../Library/UserManager.cs | 6 +- .../Cryptography/PasswordHash.cs | 75 +++++++++++-------- 5 files changed, 124 insertions(+), 98 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 4f2bc1b030..7817989e7f 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Cryptography { public class CryptographyProvider : ICryptoProvider { - private List SupportedHashMethods = new List(); + private HashSet SupportedHashMethods; public string DefaultHashMethod => "SHA256"; private RandomNumberGenerator rng; private int defaultiterations = 1000; @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Cryptography { //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one - SupportedHashMethods = new List + SupportedHashMethods = new HashSet() { "MD5" ,"System.Security.Cryptography.MD5" @@ -71,9 +71,9 @@ namespace Emby.Server.Implementations.Cryptography return SupportedHashMethods; } - private byte[] PBKDF2(string method, byte[] bytes, byte[] salt) - { - using (var r = new Rfc2898DeriveBytes(bytes, salt, defaultiterations, new HashAlgorithmName(method))) + private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + { + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations, new HashAlgorithmName(method))) { return r.GetBytes(32); } @@ -102,30 +102,40 @@ namespace Emby.Server.Implementations.Cryptography } else { - return PBKDF2(HashMethod, bytes, salt); + return PBKDF2(HashMethod, bytes, salt,defaultiterations); } } else { throw new CryptographicException(String.Format("Requested hash method is not supported: {0}", HashMethod)); } - } + } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) { - return PBKDF2(DefaultHashMethod, bytes, salt); + return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); } public byte[] ComputeHash(PasswordHash hash) - { - return ComputeHash(hash.Id, hash.HashBytes, hash.SaltBytes); - } - + { + int iterations = defaultiterations; + if (!hash.Parameters.ContainsKey("iterations")) + { + hash.Parameters.Add("iterations", defaultiterations.ToString()); + } + else + { + try { iterations = int.Parse(hash.Parameters["iterations"]); } + catch (Exception e) { iterations = defaultiterations; throw new Exception($"Couldn't successfully parse iterations value from string:{hash.Parameters["iterations"]}", e); } + } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes,iterations); + } + public byte[] GenerateSalt() { - byte[] salt = new byte[8]; + byte[] salt = new byte[64]; rng.GetBytes(salt); return salt; - } + } } } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index db359d7ddc..b3d4573427 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -55,6 +55,7 @@ namespace Emby.Server.Implementations.Data { TryMigrateToLocalUsersTable(connection); } + RemoveEmptyPasswordHashes(); } } @@ -73,6 +74,37 @@ namespace Emby.Server.Implementations.Data } } + private void RemoveEmptyPasswordHashes() + { + foreach (var user in RetrieveAllUsers()) + { + // If the user password is the sha1 hash of the empty string, remove it + if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709") || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709")) + { + continue; + } + + user.Password = null; + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + + } + /// /// Save a user in the repo /// diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 255fd82527..ca6217016b 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -36,32 +36,27 @@ namespace Emby.Server.Implementations.Library bool success = false; if (resolvedUser == null) { - success = false; throw new Exception("Invalid username or password"); } ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - - if (!resolvedUser.Password.Contains("$")) - { - ConvertPasswordFormat(resolvedUser); - } - PasswordHash ReadyHash = new PasswordHash(resolvedUser.Password); + + PasswordHash readyHash = new PasswordHash(resolvedUser.Password); byte[] CalculatedHash; string CalculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == ReadyHash.Id)) + if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == readyHash.Id)) { - if (String.IsNullOrEmpty(ReadyHash.Salt)) + if (String.IsNullOrEmpty(readyHash.Salt)) { - CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } else { - CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes, ReadyHash.SaltBytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } - if (CalculatedHashString == ReadyHash.Hash) + if (CalculatedHashString == readyHash.Hash) { success = true; //throw new Exception("Invalid username or password"); @@ -69,8 +64,7 @@ namespace Emby.Server.Implementations.Library } else { - success = false; - throw new Exception(String.Format("Requested crypto method not available in provider: {0}", ReadyHash.Id)); + throw new Exception(String.Format("Requested crypto method not available in provider: {0}", readyHash.Id)); } //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); @@ -105,26 +99,6 @@ namespace Emby.Server.Implementations.Library } } - // OLD VERSION //public Task Authenticate(string username, string password, User resolvedUser) - // OLD VERSION //{ - // OLD VERSION // if (resolvedUser == null) - // OLD VERSION // { - // OLD VERSION // throw new Exception("Invalid username or password"); - // OLD VERSION // } - // OLD VERSION // - // OLD VERSION // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - // OLD VERSION // - // OLD VERSION // if (!success) - // OLD VERSION // { - // OLD VERSION // throw new Exception("Invalid username or password"); - // OLD VERSION // } - // OLD VERSION // - // OLD VERSION // return Task.FromResult(new ProviderAuthenticationResult - // OLD VERSION // { - // OLD VERSION // Username = username - // OLD VERSION // }); - // OLD VERSION //} - public Task HasPassword(User user) { var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); @@ -133,7 +107,7 @@ namespace Emby.Server.Implementations.Library private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(passwordHash); } public Task ChangePassword(User user, string newPassword) @@ -144,7 +118,7 @@ namespace Emby.Server.Implementations.Library if(passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = BitConverter.ToString(passwordHash.SaltBytes).Replace("-",""); + passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); passwordHash.Id = _cryptographyProvider.DefaultHashMethod; passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); }else if (newPassword != null) @@ -164,19 +138,18 @@ namespace Emby.Server.Implementations.Library public string GetPasswordHash(User user) { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; + return user.Password; } public string GetEmptyHashedString(User user) { - return GetHashedString(user, string.Empty); + return null; } - public string GetHashedStringChangeAuth(string NewPassword, PasswordHash passwordHash) + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(NewPassword), passwordHash.SaltBytes)).Replace("-", string.Empty); + passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } /// @@ -184,8 +157,6 @@ namespace Emby.Server.Implementations.Library /// public string GetHashedString(User user, string str) { - //This is legacy. Deprecated in the auth method. - //return BitConverter.ToString(_cryptoProvider2.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); PasswordHash passwordHash; if (String.IsNullOrEmpty(user.Password)) { @@ -197,13 +168,15 @@ namespace Emby.Server.Implementations.Library passwordHash = new PasswordHash(user.Password); } if (passwordHash.SaltBytes != null) - { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str), passwordHash.SaltBytes)).Replace("-",string.Empty); + { + //the password is modern format with PBKDF and we should take advantage of that + passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } else - { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); - //throw new Exception("User does not have a hash, this should not be possible"); + { + //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))); } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index a139c4e736..b8777a4802 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -217,9 +217,8 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) + public static bool IsValidUsername(string username) { - //The old way was dumb, we should make it less dumb, lets do so. //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 string UserNameRegex = "^[\\w-'._@]*$"; @@ -229,8 +228,7 @@ namespace Emby.Server.Implementations.Library private static bool IsValidUsernameCharacter(char i) { - string UserNameRegex = "^[\\w-'._@]*$"; - return Regex.IsMatch(i.ToString(), UserNameRegex); + return IsValidUsername(i.ToString()); } public string MakeValidUsername(string username) diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 524484b107..cd61657c18 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -16,70 +16,78 @@ namespace MediaBrowser.Model.Cryptography public byte[] SaltBytes; public string Hash; public byte[] HashBytes; - public PasswordHash(string StorageString) + public PasswordHash(string storageString) { - string[] a = StorageString.Split('$'); - Id = a[1]; - if (a[2].Contains("=")) + string[] SplitStorageString = storageString.Split('$'); + Id = SplitStorageString[1]; + if (SplitStorageString[2].Contains("=")) { - foreach (string paramset in (a[2].Split(','))) + foreach (string paramset in (SplitStorageString[2].Split(','))) { if (!String.IsNullOrEmpty(paramset)) { - string[] fields = paramset.Split('='); - Parameters.Add(fields[0], fields[1]); + string[] fields = paramset.Split('='); + if(fields.Length == 2) + { + Parameters.Add(fields[0], fields[1]); + } } } - if (a.Length == 4) + if (SplitStorageString.Length == 5) { - Salt = a[2]; - SaltBytes = FromByteString(Salt); - Hash = a[3]; - HashBytes = FromByteString(Hash); + Salt = SplitStorageString[3]; + SaltBytes = ConvertFromByteString(Salt); + Hash = SplitStorageString[4]; + HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = a[3]; - HashBytes = FromByteString(Hash); + Hash = SplitStorageString[3]; + HashBytes = ConvertFromByteString(Hash); } } else { - if (a.Length == 4) + if (SplitStorageString.Length == 4) { - Salt = a[2]; - SaltBytes = FromByteString(Salt); - Hash = a[3]; - HashBytes = FromByteString(Hash); + Salt = SplitStorageString[2]; + SaltBytes = ConvertFromByteString(Salt); + Hash = SplitStorageString[3]; + HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = a[2]; - HashBytes = FromByteString(Hash); + Hash = SplitStorageString[2]; + HashBytes = ConvertFromByteString(Hash); } } } - public PasswordHash(ICryptoProvider cryptoProvider2) + public PasswordHash(ICryptoProvider cryptoProvider) { - Id = "SHA256"; - SaltBytes = cryptoProvider2.GenerateSalt(); - Salt = BitConverter.ToString(SaltBytes).Replace("-", ""); + Id = cryptoProvider.DefaultHashMethod; + SaltBytes = cryptoProvider.GenerateSalt(); + Salt = ConvertToByteString(SaltBytes); } - private byte[] FromByteString(string ByteString) + public static byte[] ConvertFromByteString(string byteString) { List Bytes = new List(); - for (int i = 0; i < ByteString.Length; i += 2) + for (int i = 0; i < byteString.Length; i += 2) { - Bytes.Add(Convert.ToByte(ByteString.Substring(i, 2),16)); + Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } return Bytes.ToArray(); - } + } + public static string ConvertToByteString(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", ""); + } + private string SerializeParameters() { string ReturnString = String.Empty; @@ -98,10 +106,15 @@ namespace MediaBrowser.Model.Cryptography { string OutString = "$"; OutString += Id; - if (!string.IsNullOrEmpty(SerializeParameters())) - OutString += $"${SerializeParameters()}"; + string paramstring = SerializeParameters(); + if (!string.IsNullOrEmpty(paramstring)) + { + OutString += $"${paramstring}"; + } if (!string.IsNullOrEmpty(Salt)) + { OutString += $"${Salt}"; + } OutString += $"${Hash}"; return OutString; } From 9e58e31de08b4d8e7922038a9f291e720a778b8f Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 13 Feb 2019 00:43:48 -0800 Subject: [PATCH 0007/6208] Update Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs fix to styling Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultAuthenticationProvider.cs | 226 +++++++++--------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index ca6217016b..750807b10c 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,39 +1,39 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - public DefaultAuthenticationProvider(ICryptoProvider crypto) - { - _cryptographyProvider = crypto; - } - - 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 - public Task Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - - //This is the verson that we need to use for local users. Because reasons. +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider crypto) + { + _cryptographyProvider = crypto; + } + + 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 + public Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + + //This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) - { - bool success = false; + { + bool success = false; if (resolvedUser == null) { throw new Exception("Invalid username or password"); @@ -42,18 +42,18 @@ namespace Emby.Server.Implementations.Library byte[] passwordbytes = Encoding.UTF8.GetBytes(password); PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] CalculatedHash; + byte[] CalculatedHash; string CalculatedHashString; if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == readyHash.Id)) { if (String.IsNullOrEmpty(readyHash.Salt)) { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } else { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } if (CalculatedHashString == readyHash.Hash) @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library } else { - throw new Exception(String.Format("Requested crypto method not available in provider: {0}", readyHash.Id)); + throw new Exception(String.Format("Requested crypto method not available in provider: {0}", readyHash.Id)); } //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); @@ -78,10 +78,10 @@ namespace Emby.Server.Implementations.Library { Username = username }); - } - - //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. + } + + //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)) @@ -90,71 +90,71 @@ namespace Emby.Server.Implementations.Library { string hash = user.Password; user.Password = String.Format("$SHA1${0}", hash); - } + } if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) { string hash = user.EasyPassword; user.EasyPassword = String.Format("$SHA1${0}", hash); } } - } - - public Task HasPassword(User user) - { - var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); - return Task.FromResult(hasConfiguredPassword); - } - - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.IsNullOrEmpty(passwordHash); - } - - public Task ChangePassword(User user, string newPassword) - { - //string newPasswordHash = null; - ConvertPasswordFormat(user); - PasswordHash passwordHash = new PasswordHash(user.Password); - if(passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) - { - passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); - passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); - }else if (newPassword != null) - { - passwordHash.Hash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(passwordHash.Hash)) - { - throw new ArgumentNullException(nameof(passwordHash.Hash)); - } - - user.Password = passwordHash.ToString(); - - return Task.CompletedTask; - } - - public string GetPasswordHash(User user) - { - return user.Password; - } - - public string GetEmptyHashedString(User user) - { - return null; - } - - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) - { - passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); - } - - /// - /// Gets the hashed string. - /// + } + + public Task HasPassword(User user) + { + var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); + return Task.FromResult(hasConfiguredPassword); + } + + private bool IsPasswordEmpty(User user, string passwordHash) + { + return string.IsNullOrEmpty(passwordHash); + } + + public Task ChangePassword(User user, string newPassword) + { + //string newPasswordHash = null; + ConvertPasswordFormat(user); + PasswordHash passwordHash = new PasswordHash(user.Password); + if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) + { + passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); + passwordHash.Id = _cryptographyProvider.DefaultHashMethod; + passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); + }else if (newPassword != null) + { + passwordHash.Hash = GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(passwordHash.Hash)) + { + throw new ArgumentNullException(nameof(passwordHash.Hash)); + } + + user.Password = passwordHash.ToString(); + + return Task.CompletedTask; + } + + public string GetPasswordHash(User user) + { + return user.Password; + } + + public string GetEmptyHashedString(User user) + { + return null; + } + + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) + { + passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); + } + + /// + /// Gets the hashed string. + /// public string GetHashedString(User user, string str) { PasswordHash passwordHash; @@ -163,23 +163,23 @@ namespace Emby.Server.Implementations.Library passwordHash = new PasswordHash(_cryptographyProvider); } else - { + { ConvertPasswordFormat(user); passwordHash = new PasswordHash(user.Password); } if (passwordHash.SaltBytes != null) - { - //the password is modern format with PBKDF and we should take advantage of that - passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); + { + //the password is modern format with PBKDF and we should take advantage of that + passwordHash.HashBytes = 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))); - } - - - } - } -} + } + + + } + } +} From d8e6808d77eb70025b4a26538a1deb814cbe2831 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 13 Feb 2019 00:44:07 -0800 Subject: [PATCH 0008/6208] Update Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs fix to styling Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultAuthenticationProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 750807b10c..33428c05ee 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -91,6 +91,7 @@ namespace Emby.Server.Implementations.Library string hash = user.Password; user.Password = String.Format("$SHA1${0}", hash); } + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) { string hash = user.EasyPassword; From bca569da420075030fd7de6f9ed8d3abcb7a67cb Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 13 Feb 2019 22:10:37 +0100 Subject: [PATCH 0009/6208] Reduce the amount of db calls during the post scan event --- .../Channels/ChannelPostScanTask.cs | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index ad6c537ef0..ec85ffa4d4 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels public static string GetUserDistinctValue(User user) { var channels = user.Policy.EnabledChannels - .OrderBy(i => i) - .ToList(); + .OrderBy(i => i); - return string.Join("|", channels.ToArray()); + return string.Join("|", channels); } private void CleanDatabase(CancellationToken cancellationToken) { var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); - var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery + var databaseIds = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Channel).Name } + IncludeItemTypes = new[] { typeof(Channel).Name }, + ExcludeItemIds = installedChannelIds.ToArray() }); - var invalidIds = databaseIds - .Except(installedChannelIds) - .ToList(); - - foreach (var id in invalidIds) + foreach (var channel in databaseIds.Cast()) { cancellationToken.ThrowIfCancellationRequested(); - CleanChannel(id, cancellationToken); + CleanChannel(channel, cancellationToken); } } - private void CleanChannel(Guid id, CancellationToken cancellationToken) + private void CleanChannel(Channel channel, CancellationToken cancellationToken) { - _logger.LogInformation("Cleaning channel {0} from database", id); + _logger.LogInformation("Cleaning channel {0} from database", channel.Id); // Delete all channel items - var allIds = _libraryManager.GetItemIds(new InternalItemsQuery + var items = _libraryManager.GetItemList(new InternalItemsQuery { - ChannelIds = new[] { id } + ChannelIds = new[] { channel.Id } }); - foreach (var deleteId in allIds) + foreach (var item in items) { cancellationToken.ThrowIfCancellationRequested(); - DeleteItem(deleteId); + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + + }, false); } // Finally, delete the channel itself - DeleteItem(id); - } - - private void DeleteItem(Guid id) - { - var item = _libraryManager.GetItemById(id); - - if (item == null) - { - return; - } - - _libraryManager.DeleteItem(item, new DeleteOptions + _libraryManager.DeleteItem(channel, new DeleteOptions { DeleteFileLocation = false From 0fbc4545d1b9f8b3e5af3324d92ab0c0ef0fafe2 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 14 Feb 2019 17:02:46 +0100 Subject: [PATCH 0010/6208] Address comments --- Emby.Server.Implementations/Channels/ChannelPostScanTask.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index ec85ffa4d4..3c7cbb1159 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -44,17 +44,17 @@ namespace Emby.Server.Implementations.Channels { var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); - var databaseIds = _libraryManager.GetItemList(new InternalItemsQuery + var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Channel).Name }, ExcludeItemIds = installedChannelIds.ToArray() }); - foreach (var channel in databaseIds.Cast()) + foreach (var channel in uninstalledChannels) { cancellationToken.ThrowIfCancellationRequested(); - CleanChannel(channel, cancellationToken); + CleanChannel((Channel)channel, cancellationToken); } } From a6bde0943e7feda25eba15e23a2307227de87d4e Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Thu, 14 Feb 2019 22:01:09 +0000 Subject: [PATCH 0011/6208] Implement proper FFmpeg version checking Three routes to determine FFmpeg version: 1) Grab the 'ffmpeg version x.y' from from the -version output. This should work for all pre-built binaries. 2) Compare the library versions against known contents of FFmpeg versions. This is fallback aimed at custom builds. 3) Compare libavcodec version to determine if newer than latest known release. This suggests user is running within latest/HEAD/master build. --- .../Encoder/EncoderValidator.cs | 188 +++++++++++++++++- 1 file changed, 181 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 262772959a..275062df5f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; using MediaBrowser.Model.Diagnostics; @@ -8,6 +8,71 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { + public class FFmpegVersion + { + private int _version; + private int _multi => 100; + private const int _unknown = 0; + private const int _experimental = -1; + + public FFmpegVersion(int p1) + { + _version = p1; + } + + public FFmpegVersion(string p1) + { + var match = Regex.Match(p1, @"(?\d+)\.(?\d+)"); + + if (match.Groups["major"].Success && match.Groups["minor"].Success) + { + int major = int.Parse(match.Groups["major"].Value); + int minor = int.Parse(match.Groups["minor"].Value); + _version = (major * _multi) + minor; + } + } + + public override string ToString() + { + switch (_version) + { + case _unknown: + return "Unknown"; + case _experimental: + return "Experimental"; + default: + string major = Convert.ToString(_version / _multi); + string minor = Convert.ToString(_version % _multi); + return string.Concat(major, ".", minor); + } + } + + public bool Unknown() + { + return _version == _unknown; + } + + public int Version() + { + return _version; + } + + public bool Experimental() + { + return _version == _experimental; + } + + public bool Below(FFmpegVersion checkAgainst) + { + return (_version > 0) && (_version < checkAgainst._version); + } + + public bool Suitable(FFmpegVersion checkAgainst) + { + return (_version > 0) && (_version >= checkAgainst._version); + } + } + public class EncoderValidator { private readonly ILogger _logger; @@ -58,18 +123,127 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - output = " " + output + " "; + // The minimum FFmpeg version required to run jellyfin successfully + FFmpegVersion required = new FFmpegVersion("4.0"); - for (var i = 2013; i <= 2015; i++) + // Work out what the version under test is + FFmpegVersion underTest = GetFFmpegVersion(output); + + if (logOutput) { - var yearString = i.ToString(CultureInfo.InvariantCulture); - if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1) + if (underTest.Unknown()) { - return false; + _logger.LogWarning("FFmpeg validation: Unknown version"); + } + else if (underTest.Below(required)) + { + _logger.LogWarning("FFmpeg validation: Found version {0} which is below the minimum recommended of {1}", + underTest.ToString(), required.ToString()); + } + else if (underTest.Experimental()) + { + _logger.LogWarning("FFmpeg validation: Unknown version: {0}?", underTest.ToString()); + } + else + { + _logger.LogInformation("FFmpeg validation: Detected version {0}", underTest.ToString()); } } - return true; + return underTest.Suitable(required); + } + + /// + /// Using the output from "ffmpeg -version" work out the FFmpeg version. + /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy + /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions. + /// If that fails then we use one of the main libraries to determine if it's new/older than the latest + /// we have stored. + /// + /// + /// + static private FFmpegVersion GetFFmpegVersion(string output) + { + // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output + var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)"); + + if (match.Success) + { + return new FFmpegVersion(match.Groups[1].Value); + } + else + { + // Try and use the individual library versions to determine a FFmpeg version + // This lookup table is to be maintained with the following command line: + // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' + ReadOnlyDictionary lut = new ReadOnlyDictionary + (new Dictionary + { + { new FFmpegVersion("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, + { new FFmpegVersion("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," }, + { new FFmpegVersion("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," }, + { new FFmpegVersion("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," }, + { new FFmpegVersion("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," }, + { new FFmpegVersion("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," } + }); + + // Create a reduced version string and lookup key from dictionary + var reducedVersion = GetVersionString(output); + var found = lut.FirstOrDefault(x => x.Value == reducedVersion).Key; + + if (found != null) + { + return found; + } + else + { + // Unknown version. Test the main libavcoder version in the candidate with the + // latest from the dictionary. If candidate is greater than dictionary chances are + // the user if running HEAD/master ffmpeg build (which is probably ok) + var firstElement = lut.FirstOrDefault(); + + var reqVer = Regex.Match(firstElement.Value, @"libavcodec=(\d+\.\d+)"); + var gotVer = Regex.Match(reducedVersion, @"libavcodec=(\d+\.\d+)"); + + if (reqVer.Success && gotVer.Success) + { + var req = new FFmpegVersion(reqVer.Groups[1].Value); + var got = new FFmpegVersion(gotVer.Groups[1].Value); + + // The library versions are not comparable with the FFmpeg version so must check + // candidate (got) against value from dictionary (req). Return Experimental if suitable + if( got.Suitable(req) ) + { + return new FFmpegVersion(-1); + + } + } + } + } + + // Default to return Unknown + return new FFmpegVersion(0); + } + + /// + /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output + /// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc." + /// + /// + /// + static private string GetVersionString(string output) + { + string pattern = @"((?lib\w+)\s+(?\d+)\.\s*(?\d+))"; + RegexOptions options = RegexOptions.Multiline; + + string rc = null; + + foreach (Match m in Regex.Matches(output, pattern, options)) + { + rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ','); + } + + return rc; } private static readonly string[] requiredDecoders = new[] From d8d237f6f295990c54bb2b0e36e0be04bacbdc3b Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Fri, 15 Feb 2019 23:51:22 +0000 Subject: [PATCH 0012/6208] Review comments Addressed review comments from JustAMan. Removed code to determine experimental version. Store major and minor as two ints. Allow control of a min and max recommended version. --- .../Encoder/EncoderValidator.cs | 120 +++++++----------- 1 file changed, 47 insertions(+), 73 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 275062df5f..ad777a9aa9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -10,15 +10,10 @@ namespace MediaBrowser.MediaEncoding.Encoder { public class FFmpegVersion { - private int _version; - private int _multi => 100; - private const int _unknown = 0; - private const int _experimental = -1; + private readonly int _major; + private readonly int _minor; - public FFmpegVersion(int p1) - { - _version = p1; - } + private const int _unknown = 0; public FFmpegVersion(string p1) { @@ -26,50 +21,50 @@ namespace MediaBrowser.MediaEncoding.Encoder if (match.Groups["major"].Success && match.Groups["minor"].Success) { - int major = int.Parse(match.Groups["major"].Value); - int minor = int.Parse(match.Groups["minor"].Value); - _version = (major * _multi) + minor; + _major = int.Parse(match.Groups["major"].Value); + _minor = int.Parse(match.Groups["minor"].Value); + } + else + { + _major = _unknown; + _minor = _unknown; } } public override string ToString() { - switch (_version) + switch (_major) { case _unknown: return "Unknown"; - case _experimental: - return "Experimental"; default: - string major = Convert.ToString(_version / _multi); - string minor = Convert.ToString(_version % _multi); - return string.Concat(major, ".", minor); + return string.Format("{0}.{1}",_major,_minor); } } public bool Unknown() { - return _version == _unknown; - } - - public int Version() - { - return _version; - } - - public bool Experimental() - { - return _version == _experimental; + return _major == _unknown; } public bool Below(FFmpegVersion checkAgainst) { - return (_version > 0) && (_version < checkAgainst._version); + return ToScalar() < checkAgainst.ToScalar(); } - public bool Suitable(FFmpegVersion checkAgainst) + public bool Above(FFmpegVersion checkAgainst) { - return (_version > 0) && (_version >= checkAgainst._version); + return ToScalar() > checkAgainst.ToScalar(); + } + + public bool Same(FFmpegVersion checkAgainst) + { + return ToScalar() == checkAgainst.ToScalar(); + } + + private int ToScalar() + { + return (_major * 1000) + _minor; } } @@ -123,34 +118,43 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - // The minimum FFmpeg version required to run jellyfin successfully - FFmpegVersion required = new FFmpegVersion("4.0"); + // The min and max FFmpeg versions required to run jellyfin successfully + FFmpegVersion minRequired = new FFmpegVersion("4.0"); + FFmpegVersion maxRequired = new FFmpegVersion("4.0"); // Work out what the version under test is FFmpegVersion underTest = GetFFmpegVersion(output); if (logOutput) { + _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest.ToString()); + if (underTest.Unknown()) { - _logger.LogWarning("FFmpeg validation: Unknown version"); + if (minRequired.Same(maxRequired)) + { + _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString()); + } + else + { + _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString()); + } } - else if (underTest.Below(required)) + else if (underTest.Below(minRequired)) { - _logger.LogWarning("FFmpeg validation: Found version {0} which is below the minimum recommended of {1}", - underTest.ToString(), required.ToString()); + _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString()); } - else if (underTest.Experimental()) + else if (underTest.Above(maxRequired)) { - _logger.LogWarning("FFmpeg validation: Unknown version: {0}?", underTest.ToString()); + _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString()); } else { - _logger.LogInformation("FFmpeg validation: Detected version {0}", underTest.ToString()); + // Version is ok so no warning required } } - return underTest.Suitable(required); + return !underTest.Below(minRequired) && !underTest.Above(maxRequired); } /// @@ -176,7 +180,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Try and use the individual library versions to determine a FFmpeg version // This lookup table is to be maintained with the following command line: // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' - ReadOnlyDictionary lut = new ReadOnlyDictionary + var lut = new ReadOnlyDictionary (new Dictionary { { new FFmpegVersion("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, @@ -191,38 +195,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var reducedVersion = GetVersionString(output); var found = lut.FirstOrDefault(x => x.Value == reducedVersion).Key; - if (found != null) - { - return found; - } - else - { - // Unknown version. Test the main libavcoder version in the candidate with the - // latest from the dictionary. If candidate is greater than dictionary chances are - // the user if running HEAD/master ffmpeg build (which is probably ok) - var firstElement = lut.FirstOrDefault(); - - var reqVer = Regex.Match(firstElement.Value, @"libavcodec=(\d+\.\d+)"); - var gotVer = Regex.Match(reducedVersion, @"libavcodec=(\d+\.\d+)"); - - if (reqVer.Success && gotVer.Success) - { - var req = new FFmpegVersion(reqVer.Groups[1].Value); - var got = new FFmpegVersion(gotVer.Groups[1].Value); - - // The library versions are not comparable with the FFmpeg version so must check - // candidate (got) against value from dictionary (req). Return Experimental if suitable - if( got.Suitable(req) ) - { - return new FFmpegVersion(-1); - - } - } - } + return found ?? new FFmpegVersion("Unknown"); } - - // Default to return Unknown - return new FFmpegVersion(0); } /// From 69ea15f73a053e2526457b0eec757fb6f1ca1cd7 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Sat, 16 Feb 2019 00:47:38 +0000 Subject: [PATCH 0013/6208] Use string interpolation Two further review comments from JustAMan. --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index ad777a9aa9..be33ea58ef 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Encoder case _unknown: return "Unknown"; default: - return string.Format("{0}.{1}",_major,_minor); + return $"{_major}.{_minor}"; } } @@ -150,7 +150,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { - // Version is ok so no warning required + _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version"); } } From bb503638123bafb42594456fa440e71b80088fb5 Mon Sep 17 00:00:00 2001 From: bobberb Date: Sun, 17 Feb 2019 00:21:55 +0000 Subject: [PATCH 0014/6208] Translated using Weblate (Hebrew) Currently translated at 97.8% (92 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/ --- Emby.Server.Implementations/Localization/Core/he.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index fff1d1f0ec..0ed998c4bd 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -1,8 +1,8 @@ { - "Albums": "Albums", + "Albums": "אלבומים", "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", + "Application": "אפליקציה", + "Artists": "אמנים", "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "Books": "ספרים", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", From 9f3aa2cead95ec0a66a518919c179eea4cad5d9c Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Mon, 18 Feb 2019 00:31:03 -0800 Subject: [PATCH 0015/6208] Apply suggestions from code review Adding minor stylistic suggestions from Bond-009 Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Cryptography/CryptographyProvider.cs | 41 +- .../Data/SqliteUserRepository.cs | 469 +++++++++--------- .../Library/DefaultAuthenticationProvider.cs | 9 +- 3 files changed, 264 insertions(+), 255 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 7817989e7f..436443f066 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Cryptography } private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) - { + { using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations, new HashAlgorithmName(method))) { return r.GetBytes(32); @@ -107,9 +107,9 @@ namespace Emby.Server.Implementations.Cryptography } else { - throw new CryptographicException(String.Format("Requested hash method is not supported: {0}", HashMethod)); + throw new CryptographicException($"Requested hash method is not supported: {HashMethod}")); } - } + } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) { @@ -117,25 +117,32 @@ namespace Emby.Server.Implementations.Cryptography } public byte[] ComputeHash(PasswordHash hash) - { - int iterations = defaultiterations; - if (!hash.Parameters.ContainsKey("iterations")) - { - hash.Parameters.Add("iterations", defaultiterations.ToString()); - } - else - { - try { iterations = int.Parse(hash.Parameters["iterations"]); } - catch (Exception e) { iterations = defaultiterations; throw new Exception($"Couldn't successfully parse iterations value from string:{hash.Parameters["iterations"]}", e); } + { + int iterations = defaultiterations; + if (!hash.Parameters.ContainsKey("iterations")) + { + hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture)); } - return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes,iterations); - } - + else + { + try + { + iterations = int.Parse(hash.Parameters["iterations"]); + } + catch (Exception e) + { + iterations = defaultiterations; + throw new Exception($"Couldn't successfully parse iterations value from string:{hash.Parameters["iterations"]}", e); + } + } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); + } + public byte[] GenerateSalt() { byte[] salt = new byte[64]; rng.GetBytes(salt); return salt; - } + } } } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index b3d4573427..1b6deae7d3 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -1,85 +1,86 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteUserRepository - /// - public class SqliteUserRepository : BaseSqliteRepository, IUserRepository - { - private readonly IJsonSerializer _jsonSerializer; - - public SqliteUserRepository( - ILoggerFactory loggerFactory, - IServerApplicationPaths appPaths, - IJsonSerializer jsonSerializer) - : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) - { - _jsonSerializer = jsonSerializer; - - DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name => "SQLite"; - - /// - /// Opens the connection to the database - /// - /// Task. - public void Initialize() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - var localUsersTableExists = TableExists(connection, "LocalUsersv2"); - - connection.RunQueries(new[] { - "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", - "drop index if exists idx_users" - }); - - if (!localUsersTableExists && TableExists(connection, "Users")) - { - TryMigrateToLocalUsersTable(connection); - } - RemoveEmptyPasswordHashes(); - } - } - - private void TryMigrateToLocalUsersTable(ManagedConnection connection) - { - try - { - connection.RunQueries(new[] - { - "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating users database"); - } - } - +using System; +using System.Collections.Generic; +using System.IO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteUserRepository + /// + public class SqliteUserRepository : BaseSqliteRepository, IUserRepository + { + private readonly IJsonSerializer _jsonSerializer; + + public SqliteUserRepository( + ILoggerFactory loggerFactory, + IServerApplicationPaths appPaths, + IJsonSerializer jsonSerializer) + : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) + { + _jsonSerializer = jsonSerializer; + + DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name => "SQLite"; + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + var localUsersTableExists = TableExists(connection, "LocalUsersv2"); + + connection.RunQueries(new[] { + "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", + "drop index if exists idx_users" + }); + + if (!localUsersTableExists && TableExists(connection, "Users")) + { + TryMigrateToLocalUsersTable(connection); + } + RemoveEmptyPasswordHashes(); + } + } + + private void TryMigrateToLocalUsersTable(ManagedConnection connection) + { + try + { + connection.RunQueries(new[] + { + "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" + }); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error migrating users database"); + } + } + private void RemoveEmptyPasswordHashes() { foreach (var user in RetrieveAllUsers()) { // If the user password is the sha1 hash of the empty string, remove it - if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709") || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709")) + if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) + || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) { continue; } @@ -103,160 +104,160 @@ namespace Emby.Server.Implementations.Data } } - } - - /// - /// Save a user in the repo - /// - public void CreateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) - { - statement.TryBind("@guid", user.Id.ToGuidBlob()); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - - var createdUser = GetUser(user.Id, false); - - if (createdUser == null) - { - throw new ApplicationException("created user should never be null"); - } - - user.InternalId = createdUser.InternalId; - - }, TransactionMode); - } - } - } - - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - - }, TransactionMode); - } - } - } - - private User GetUser(Guid guid, bool openLock) - { - using (openLock ? WriteLock.Read() : null) - { - using (var connection = CreateConnection(true)) - { - using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) - { - statement.TryBind("@guid", guid); - - foreach (var row in statement.ExecuteQuery()) - { - return GetUser(row); - } - } - } - } - - return null; - } - - private User GetUser(IReadOnlyList row) - { - var id = row[0].ToInt64(); - var guid = row[1].ReadGuidFromBlob(); - - using (var stream = new MemoryStream(row[2].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream(stream); - user.InternalId = id; - user.Id = guid; - return user; - } - } - - /// - /// Retrieve all users from the database - /// - /// IEnumerable{User}. - public List RetrieveAllUsers() - { - var list = new List(); - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) - { - list.Add(GetUser(row)); - } - } - } - - return list; - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - public void DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) - { - statement.TryBind("@id", user.InternalId); - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - } -} + } + + /// + /// Save a user in the repo + /// + public void CreateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) + { + statement.TryBind("@guid", user.Id.ToGuidBlob()); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } + + var createdUser = GetUser(user.Id, false); + + if (createdUser == null) + { + throw new ApplicationException("created user should never be null"); + } + + user.InternalId = createdUser.InternalId; + + }, TransactionMode); + } + } + } + + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + } + + private User GetUser(Guid guid, bool openLock) + { + using (openLock ? WriteLock.Read() : null) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) + { + statement.TryBind("@guid", guid); + + foreach (var row in statement.ExecuteQuery()) + { + return GetUser(row); + } + } + } + } + + return null; + } + + private User GetUser(IReadOnlyList row) + { + var id = row[0].ToInt64(); + var guid = row[1].ReadGuidFromBlob(); + + using (var stream = new MemoryStream(row[2].ToBlob())) + { + stream.Position = 0; + var user = _jsonSerializer.DeserializeFromStream(stream); + user.InternalId = id; + user.Id = guid; + return user; + } + } + + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + public List RetrieveAllUsers() + { + var list = new List(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) + { + list.Add(GetUser(row)); + } + } + } + + return list; + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + public void DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) + { + statement.TryBind("@id", user.InternalId); + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + } +} diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 33428c05ee..016de6db78 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Library PasswordHash readyHash = new PasswordHash(resolvedUser.Password); byte[] CalculatedHash; string CalculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == readyHash.Id)) + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) { if (String.IsNullOrEmpty(readyHash.Salt)) { @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library } else { - throw new Exception(String.Format("Requested crypto method not available in provider: {0}", readyHash.Id)); + throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); } //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Library if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) { string hash = user.EasyPassword; - user.EasyPassword = String.Format("$SHA1${0}", hash); + user.EasyPassword = string.Format("$SHA1${0}", hash); } } } @@ -122,7 +122,8 @@ namespace Emby.Server.Implementations.Library passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); passwordHash.Id = _cryptographyProvider.DefaultHashMethod; passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); - }else if (newPassword != null) + } + else if (newPassword != null) { passwordHash.Hash = GetHashedString(user, newPassword); } From 48e7274d3783e57f89f6e1cc76fcd8696e987ec5 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 18 Feb 2019 01:26:01 -0800 Subject: [PATCH 0016/6208] added justaman notes, fixed new bug from emty has removals --- .../Cryptography/CryptographyProvider.cs | 5 ++- .../Library/DefaultAuthenticationProvider.cs | 42 ++++++++++++++----- .../Library/UserManager.cs | 4 +- .../Cryptography/PasswordHash.cs | 42 ++++++++++--------- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 436443f066..c4f0346314 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; @@ -102,12 +103,12 @@ namespace Emby.Server.Implementations.Cryptography } else { - return PBKDF2(HashMethod, bytes, salt,defaultiterations); + return PBKDF2(HashMethod, bytes, salt, defaultiterations); } } else { - throw new CryptographicException($"Requested hash method is not supported: {HashMethod}")); + throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); } } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 016de6db78..80026d97c9 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -37,7 +37,17 @@ namespace Emby.Server.Implementations.Library if (resolvedUser == null) { throw new Exception("Invalid username or password"); - } + } + + //As long as jellyfin supports passwordless users, we need this little block here to accomodate + if (IsPasswordEmpty(resolvedUser, password)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); @@ -106,15 +116,30 @@ namespace Emby.Server.Implementations.Library return Task.FromResult(hasConfiguredPassword); } - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.IsNullOrEmpty(passwordHash); + private bool IsPasswordEmpty(User user, string password) + { + if (string.IsNullOrEmpty(user.Password)) + { + return string.IsNullOrEmpty(password); + } + return false; } public Task ChangePassword(User user, string newPassword) { - //string newPasswordHash = null; - ConvertPasswordFormat(user); + ConvertPasswordFormat(user); + //This is needed to support changing a no password user to a password user + if (string.IsNullOrEmpty(user.Password)) + { + PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); + newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); + newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; + newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); + user.Password = newPasswordHash.ToString(); + return Task.CompletedTask; + } + PasswordHash passwordHash = new PasswordHash(user.Password); if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { @@ -143,11 +168,6 @@ namespace Emby.Server.Implementations.Library return user.Password; } - public string GetEmptyHashedString(User user) - { - return null; - } - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index b8777a4802..3daed0c085 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -475,13 +475,13 @@ namespace Emby.Server.Implementations.Library private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + ? null : user.EasyPassword; } private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(passwordHash); } /// diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index cd61657c18..3a817543bc 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -18,48 +18,52 @@ namespace MediaBrowser.Model.Cryptography public byte[] HashBytes; public PasswordHash(string storageString) { - string[] SplitStorageString = storageString.Split('$'); - Id = SplitStorageString[1]; - if (SplitStorageString[2].Contains("=")) + string[] splitted = storageString.Split('$'); + Id = splitted[1]; + if (splitted[2].Contains("=")) { - foreach (string paramset in (SplitStorageString[2].Split(','))) + foreach (string paramset in (splitted[2].Split(','))) { if (!String.IsNullOrEmpty(paramset)) { string[] fields = paramset.Split('='); - if(fields.Length == 2) + if (fields.Length == 2) { Parameters.Add(fields[0], fields[1]); + } + else + { + throw new Exception($"Malformed parameter in password hash string {paramset}"); } } } - if (SplitStorageString.Length == 5) + if (splitted.Length == 5) { - Salt = SplitStorageString[3]; + Salt = splitted[3]; SaltBytes = ConvertFromByteString(Salt); - Hash = SplitStorageString[4]; + Hash = splitted[4]; HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = SplitStorageString[3]; + Hash = splitted[3]; HashBytes = ConvertFromByteString(Hash); } } else { - if (SplitStorageString.Length == 4) + if (splitted.Length == 4) { - Salt = SplitStorageString[2]; + Salt = splitted[2]; SaltBytes = ConvertFromByteString(Salt); - Hash = SplitStorageString[3]; + Hash = splitted[3]; HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = SplitStorageString[2]; + Hash = splitted[2]; HashBytes = ConvertFromByteString(Hash); } @@ -83,6 +87,7 @@ namespace MediaBrowser.Model.Cryptography } return Bytes.ToArray(); } + public static string ConvertToByteString(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", ""); @@ -104,19 +109,18 @@ namespace MediaBrowser.Model.Cryptography public override string ToString() { - string OutString = "$"; - OutString += Id; + string outString = "$" +Id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { - OutString += $"${paramstring}"; + outString += $"${paramstring}"; } if (!string.IsNullOrEmpty(Salt)) { - OutString += $"${Salt}"; + outString += $"${Salt}"; } - OutString += $"${Hash}"; - return OutString; + outString += $"${Hash}"; + return outString; } } From 56e306334201e06f2066e1e7ca1246508346bfa9 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 18 Feb 2019 10:56:01 -0800 Subject: [PATCH 0017/6208] little fixes for JustAMan --- .../Cryptography/CryptographyProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index c4f0346314..dc528c280a 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -132,8 +132,7 @@ namespace Emby.Server.Implementations.Cryptography } catch (Exception e) { - iterations = defaultiterations; - throw new Exception($"Couldn't successfully parse iterations value from string:{hash.Parameters["iterations"]}", e); + throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); } } return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); From 6834b64922d2dd10b8e27943ec6630d1d95d6ae5 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 22:26:40 +0100 Subject: [PATCH 0018/6208] Add first Azure Pipeline (Build) --- .azure/azure-pipelines.yml | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .azure/azure-pipelines.yml diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml new file mode 100644 index 0000000000..ad6ba1ab27 --- /dev/null +++ b/.azure/azure-pipelines.yml @@ -0,0 +1,50 @@ +resources: +- repo: self + +pool: + vmImage: Hosted Ubuntu 1604 +#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 +#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 +#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 +#Your build pipeline references an undefined variable named ‘Parameters.TestProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 +#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 +#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 +#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 + +steps: +- task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: restore + projects: '$(Parameters.RestoreBuildProjects)' + +- task: DotNetCoreCLI@2 + displayName: Build + inputs: + projects: '$(Parameters.RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(Parameters.TestProjects)' + arguments: '--configuration $(BuildConfiguration)' + enabled: false + +- task: DotNetCoreCLI@2 + displayName: Publish + inputs: + command: publish + publishWebProjects: false + projects: '$(Parameters.RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + zipAfterPublish: True + +- task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + enabled: false + + From 18717d103af8f1360360bd1bb52e50c7bd4217fa Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 22:36:24 +0100 Subject: [PATCH 0019/6208] Update azure-pipelines.yml for Azure Pipelines --- .azure/azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index ad6ba1ab27..513c7b6fc3 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,6 +1,12 @@ resources: - repo: self +pr: + branches: + include: + - master + - releases/* + pool: vmImage: Hosted Ubuntu 1604 #Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 From c6188e26afa0034c5c255a19b2fc71aa42311e26 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 18 Feb 2019 22:47:02 +0100 Subject: [PATCH 0020/6208] Got to start somewhere --- .../ApplicationHost.cs | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7d77500abc..1b88fae0e5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -181,11 +181,17 @@ namespace Emby.Server.Implementations /// The logger. protected ILogger Logger { get; set; } + private IPlugin[] _plugins; + /// /// Gets or sets the plugins. /// /// The plugins. - public IPlugin[] Plugins { get; protected set; } + public IPlugin[] Plugins + { + get => _plugins; + protected set => _plugins = value; + } /// /// Gets or sets the logger factory. @@ -1047,6 +1053,41 @@ namespace Emby.Server.Implementations CollectionFolder.JsonSerializer = JsonSerializer; CollectionFolder.ApplicationHost = this; AuthenticatedAttribute.AuthService = AuthService; + + InstallationManager.PluginInstalled += PluginInstalled; + } + + private async void PluginInstalled(object sender, GenericEventArgs args) + { + string dir = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(args.Argument.targetFilename)); + var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly) + .Select(x => Assembly.LoadFrom(x)) + .SelectMany(x => x.ExportedTypes) + .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) + .ToList(); + + types.AddRange(types); + + var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin))) + .Select(CreateInstanceSafe) + .Where(x => x != null) + .Cast() + .Select(LoadPlugin) + .Where(x => x != null) + .ToArray(); + + int oldLen = _plugins.Length; + Array.Resize(ref _plugins, _plugins.Length + plugins.Length); + plugins.CopyTo(_plugins, oldLen); + + var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint))) + .Select(CreateInstanceSafe) + .Where(x => x != null) + .Cast() + .ToList(); + + await Task.WhenAll(StartEntryPoints(entries, true)); + await Task.WhenAll(StartEntryPoints(entries, false)); } /// From 60fc53306d3a5946dfb219da3163eaa49b99e4c5 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 22:53:32 +0100 Subject: [PATCH 0021/6208] Fixed vmImage --- .azure/azure-pipelines.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index ad6ba1ab27..7ec002dd74 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,8 +1,14 @@ resources: -- repo: self + repositories: self + +pr: +include: +- master +- release-* + pool: - vmImage: Hosted Ubuntu 1604 + vmImage: ubuntu-16.04 #Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 #Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 #Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 From ef17ec700bd18861967c7933727eb0e4a36a767a Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 23:24:39 +0100 Subject: [PATCH 0022/6208] Update azure-pipelines.yml --- .azure/azure-pipelines.yml | 72 ++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index fc239032e0..d2f4dc393a 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,17 +1,11 @@ resources: repositories: self -pr: -include: -- master -- release-* - - pr: branches: include: - master - - releases/* + - release-* pool: vmImage: ubuntu-16.04 @@ -23,40 +17,42 @@ pool: #Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 #Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 -steps: -- task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: restore - projects: '$(Parameters.RestoreBuildProjects)' +jobs: +- job: Build + steps: + - task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: restore + projects: '$(Parameters.RestoreBuildProjects)' -- task: DotNetCoreCLI@2 - displayName: Build - inputs: - projects: '$(Parameters.RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + projects: '$(Parameters.RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' -- task: DotNetCoreCLI@2 - displayName: Test - inputs: - command: test - projects: '$(Parameters.TestProjects)' - arguments: '--configuration $(BuildConfiguration)' - enabled: false + - task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(Parameters.TestProjects)' + arguments: '--configuration $(BuildConfiguration)' + enabled: false -- task: DotNetCoreCLI@2 - displayName: Publish - inputs: - command: publish - publishWebProjects: false - projects: '$(Parameters.RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' - zipAfterPublish: True + - task: DotNetCoreCLI@2 + displayName: Publish + inputs: + command: publish + publishWebProjects: false + projects: '$(Parameters.RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + zipAfterPublish: True -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - enabled: false + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + enabled: false From 480999e8e637354867f2dbf6db9216c9d7a72ebe Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 23:42:24 +0100 Subject: [PATCH 0023/6208] Update azure-pipelines.yml for Azure Pipelines --- .azure/azure-pipelines.yml | 91 +++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index d2f4dc393a..a69f0ab9fb 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,58 +1,67 @@ -resources: - repositories: self - pr: + autoCancel: true + +trigger: + batch: true branches: include: - master - - release-* pool: vmImage: ubuntu-16.04 -#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 -#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 -#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 -#Your build pipeline references an undefined variable named ‘Parameters.TestProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 -#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 -#Your build pipeline references an undefined variable named ‘Parameters.RestoreBuildProjects’. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972 + +variables: + - name: TestProjects + value: '' + - name: RestoreBuildProjects + value: 'Jellyfin.Server/Jellyfin.Server.csproj' + #Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 jobs: - job: Build - steps: - - task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: restore - projects: '$(Parameters.RestoreBuildProjects)' + steps: + - checkout: self + clean: false + submodules: true + persistCredentials: false - - task: DotNetCoreCLI@2 - displayName: Build - inputs: - projects: '$(Parameters.RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' + - task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: restore + projects: '$(RestoreBuildProjects)' - - task: DotNetCoreCLI@2 - displayName: Test - inputs: - command: test - projects: '$(Parameters.TestProjects)' - arguments: '--configuration $(BuildConfiguration)' - enabled: false + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + enabled: false + + - task: DotNetCoreCLI@2 + displayName: Publish + inputs: + command: publish + publishWebProjects: false + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + zipAfterPublish: True + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + artifactName: 'build-$(BuildConfiguration)' + zipAfterPublish: false - - task: DotNetCoreCLI@2 - displayName: Publish - inputs: - command: publish - publishWebProjects: false - projects: '$(Parameters.RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' - zipAfterPublish: True - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - enabled: false From 7c9803e135eb7c02f4b759a4b0dc1b9a55b30af1 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Mon, 18 Feb 2019 23:47:31 +0100 Subject: [PATCH 0024/6208] Update azure-pipelines.yml for Azure Pipelines --- .azure/azure-pipelines.yml | 104 +++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index a69f0ab9fb..3b48664956 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,3 +1,20 @@ +name: Build + +resources: + repositories: + - repository: self + type: github + name: EraYaN/jellyfin + +pool: + vmImage: ubuntu-16.04 + +variables: +- name: TestProjects + value: '' +- name: RestoreBuildProjects + value: 'Jellyfin.Server/Jellyfin.Server.csproj' + pr: autoCancel: true @@ -7,61 +24,48 @@ trigger: include: - master -pool: - vmImage: ubuntu-16.04 - -variables: - - name: TestProjects - value: '' - - name: RestoreBuildProjects - value: 'Jellyfin.Server/Jellyfin.Server.csproj' - #Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 jobs: -- job: Build - steps: - - checkout: self - clean: false - submodules: true - persistCredentials: false + - job: Build + steps: + - checkout: self + clean: false + submodules: true + persistCredentials: false - - task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: restore - projects: '$(RestoreBuildProjects)' - - - task: DotNetCoreCLI@2 - displayName: Build - inputs: - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' - - - task: DotNetCoreCLI@2 - displayName: Test - inputs: - command: test - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' - enabled: false - - - task: DotNetCoreCLI@2 - displayName: Publish - inputs: - command: publish - publishWebProjects: false - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' - zipAfterPublish: True - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - artifactName: 'build-$(BuildConfiguration)' - zipAfterPublish: false + - task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: restore + projects: '$(RestoreBuildProjects)' + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + - task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + enabled: false + - task: DotNetCoreCLI@2 + displayName: Publish + inputs: + command: publish + publishWebProjects: false + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + zipAfterPublish: true + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + artifactName: 'build-$(BuildConfiguration)' + zipAfterPublish: false From 7bbcb455c0b6590d3e0e52948657de74b11bb3b6 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:17:57 +0100 Subject: [PATCH 0025/6208] Added compat checking to YAML --- .azure/azure-pipelines.yml | 135 +++++++++++++++++++++++++++++++------ 1 file changed, 116 insertions(+), 19 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 3b48664956..057534608e 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,19 +1,10 @@ -name: Build - -resources: - repositories: - - repository: self - type: github - name: EraYaN/jellyfin - -pool: - vmImage: ubuntu-16.04 +name: $(Date:yyyyMMdd).$(Rev:.r) variables: -- name: TestProjects - value: '' -- name: RestoreBuildProjects - value: 'Jellyfin.Server/Jellyfin.Server.csproj' + - name: TestProjects + value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj' + - name: RestoreBuildProjects + value: 'Jellyfin.Server/Jellyfin.Server.csproj' pr: autoCancel: true @@ -22,12 +13,19 @@ trigger: batch: true branches: include: - - master - -#Your build pipeline references the ‘BuildConfiguration’ variable, which you’ve selected to be settable at queue time. Create or edit the build pipeline for this YAML file, define the variable on the Variables tab, and then select the option to make it settable at queue time. See https://go.microsoft.com/fwlink/?linkid=865971 + - master jobs: - - job: Build + - job: main_build + displayName: Main Build + pool: + vmImage: ubuntu-16.04 + strategy: + matrix: + release: + BuildConfiguration: Release + debug: + BuildConfiguration: Release steps: - checkout: self clean: false @@ -67,5 +65,104 @@ jobs: displayName: 'Publish Artifact' inputs: PathtoPublish: '$(build.artifactstagingdirectory)' - artifactName: 'build-$(BuildConfiguration)' + artifactName: 'jellyfin-build-$(BuildConfiguration)' zipAfterPublish: false + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Naming' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Naming.dll' + artifactName: 'Jellyfin.Naming' + zipAfterPublish: false + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Controller' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Controller.dll' + artifactName: 'Jellyfin.Controller' + zipAfterPublish: false + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Model' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Model.dll' + artifactName: 'Jellyfin.Model' + zipAfterPublish: false + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Common' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Common.dll' + artifactName: 'Jellyfin.Common' + zipAfterPublish: false + + - job: dotnet_compat + displayName: Compatibility Check + pool: + vmImage: ubuntu-16.04 + dependsOn: main_build + condition: success() + strategy: + matrix: + Naming: + NugetPackageName: Jellyfin.Naming + AssemblyName: Emby.Naming.dll + Controller: + NugetPackageName: Jellyfin.Controller + AssemblyName: MediaBrowser.Controller.dll + Model: + NugetPackageName: Jellyfin.Model + AssemblyName: MediaBrowser.Model.dll + Common: + NugetPackageName: Jellyfin.Common + AssemblyName: MediaBrowser.Common.dll + steps: + - checkout: none + + - task: NuGet@2 + displayName: 'Download $(NugetPackageName)' + inputs: + command: custom + arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload' + + - task: CopyFiles@2 + displayName: Copy Nuget Assembly to release folder + inputs: + sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional + contents: '**/*.dll' + targetFolder: $(System.ArtifactsDirectory)/current-release + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: true # Optional + + - task: DownloadBuildArtifacts@0 + displayName: Download the Assembly Build Artifact + inputs: + buildType: 'current' # Options: current, specific + allowPartiallySucceededBuilds: false # Optional + downloadType: 'single' # Options: single, specific + artifactName: '$(NugetPackageName)' # Required when downloadType == Single + downloadPath: '$(System.ArtifactsDirectory)/new-release' + + - task: DownloadGitHubReleases@0 + displayName: Download ABI compatibility check tool from GitHub + inputs: + connection: GitHub (EraYaN) + userRepository: EraYaN/dotnet-compatibility + defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag + #version: # Required when defaultVersionType != Latest + itemPattern: '**-ci.zip' # Optional + downloadPath: '$(System.ArtifactsDirectory)' + + - task: ExtractFiles@1 + displayName: Extract ABI compatibility check tool + inputs: + archiveFilePatterns: '*-ci.zip' + destinationFolder: $(System.ArtifactsDirectory)\tools + cleanDestinationFolder: true + + From 5ef63e738dae8c9fda0a2cee2124aff627723fda Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:28:16 +0100 Subject: [PATCH 0026/6208] Added final stage and removed triggers. --- .azure/azure-pipelines.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 057534608e..87be79d0a4 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -110,16 +110,16 @@ jobs: matrix: Naming: NugetPackageName: Jellyfin.Naming - AssemblyName: Emby.Naming.dll + AssemblyFileName: Emby.Naming.dll Controller: NugetPackageName: Jellyfin.Controller - AssemblyName: MediaBrowser.Controller.dll + AssemblyFileName: MediaBrowser.Controller.dll Model: NugetPackageName: Jellyfin.Model - AssemblyName: MediaBrowser.Model.dll + AssemblyFileName: MediaBrowser.Model.dll Common: NugetPackageName: Jellyfin.Common - AssemblyName: MediaBrowser.Common.dll + AssemblyFileName: MediaBrowser.Common.dll steps: - checkout: none @@ -162,7 +162,14 @@ jobs: displayName: Extract ABI compatibility check tool inputs: archiveFilePatterns: '*-ci.zip' - destinationFolder: $(System.ArtifactsDirectory)\tools + destinationFolder: $(System.ArtifactsDirectory)/tools cleanDestinationFolder: true + - task: CmdLine@2 + displayName: Execute ABI compatibility check tool + inputs: + script: 'tools/CompatibilityCheckerCoreCLI current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)' + workingDirectory: $(System.ArtifactsDirectory) # Optional + #failOnStderr: false # Optional + From e2d0f6707756407f76e819a1c55928f1a133292f Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:32:36 +0100 Subject: [PATCH 0027/6208] Fix the conditions. --- .azure/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 87be79d0a4..5e8a9f9360 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -105,7 +105,7 @@ jobs: pool: vmImage: ubuntu-16.04 dependsOn: main_build - condition: success() + condition: succeeded() strategy: matrix: Naming: From d4bc7b5a5ae3a5e59e367e4832d8a929d433c46f Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:42:42 +0100 Subject: [PATCH 0028/6208] Fix NuGet task name. --- .azure/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 5e8a9f9360..38a355b96f 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -123,7 +123,7 @@ jobs: steps: - checkout: none - - task: NuGet@2 + - task: NuGetCommand@2 displayName: 'Download $(NugetPackageName)' inputs: command: custom From 18418b68926f42a16ba848ba2dc7135f4473682d Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:50:25 +0100 Subject: [PATCH 0029/6208] Fixed secondary Artifact paths and Debug config. --- .azure/azure-pipelines.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 38a355b96f..500d9d8c80 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -25,7 +25,7 @@ jobs: release: BuildConfiguration: Release debug: - BuildConfiguration: Release + BuildConfiguration: Debug steps: - checkout: self clean: false @@ -66,39 +66,35 @@ jobs: inputs: PathtoPublish: '$(build.artifactstagingdirectory)' artifactName: 'jellyfin-build-$(BuildConfiguration)' - zipAfterPublish: false + zipAfterPublish: true - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Naming' condition: eq(variables['BuildConfiguration'], 'Release') inputs: - PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Naming.dll' + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Naming.dll' artifactName: 'Jellyfin.Naming' - zipAfterPublish: false - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Controller' condition: eq(variables['BuildConfiguration'], 'Release') inputs: - PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Controller.dll' + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' artifactName: 'Jellyfin.Controller' - zipAfterPublish: false - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Model' condition: eq(variables['BuildConfiguration'], 'Release') inputs: - PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Model.dll' + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll' artifactName: 'Jellyfin.Model' - zipAfterPublish: false - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Common' condition: eq(variables['BuildConfiguration'], 'Release') inputs: - PathtoPublish: '$(build.artifactstagingdirectory)/MediaBrowser.Common.dll' + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll' artifactName: 'Jellyfin.Common' - zipAfterPublish: false - job: dotnet_compat displayName: Compatibility Check From 9961c8c4597c0e83d88491ca87e3e7535f27a661 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:55:10 +0100 Subject: [PATCH 0030/6208] No full publish of artifacts. --- .azure/azure-pipelines.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 500d9d8c80..9fbb979676 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -59,14 +59,14 @@ jobs: publishWebProjects: false projects: '$(RestoreBuildProjects)' arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' - zipAfterPublish: true + zipAfterPublish: false - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - artifactName: 'jellyfin-build-$(BuildConfiguration)' - zipAfterPublish: true + # - task: PublishBuildArtifacts@1 + # displayName: 'Publish Artifact' + # inputs: + # PathtoPublish: '$(build.artifactstagingdirectory)' + # artifactName: 'jellyfin-build-$(BuildConfiguration)' + # zipAfterPublish: true - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Naming' From 6b1a64652fcbf087cca6a1c1817189d852797937 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 02:59:56 +0100 Subject: [PATCH 0031/6208] Fix path for naming, and add extra CopyFIle Task. --- .azure/azure-pipelines.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 9fbb979676..3e133290c4 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -72,7 +72,7 @@ jobs: displayName: 'Publish Artifact Naming' condition: eq(variables['BuildConfiguration'], 'Release') inputs: - PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Naming.dll' + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll' artifactName: 'Jellyfin.Naming' - task: PublishBuildArtifacts@1 @@ -126,7 +126,7 @@ jobs: arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload' - task: CopyFiles@2 - displayName: Copy Nuget Assembly to release folder + displayName: Copy Nuget Assembly to current-release folder inputs: sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional contents: '**/*.dll' @@ -142,7 +142,17 @@ jobs: allowPartiallySucceededBuilds: false # Optional downloadType: 'single' # Options: single, specific artifactName: '$(NugetPackageName)' # Required when downloadType == Single - downloadPath: '$(System.ArtifactsDirectory)/new-release' + downloadPath: '$(System.ArtifactsDirectory)/new-artifacts' + + - task: CopyFiles@2 + displayName: Copy Artifact Assembly to new-release folder + inputs: + sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional + contents: '**/*.dll' + targetFolder: $(System.ArtifactsDirectory)/new-release + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: true # Optional - task: DownloadGitHubReleases@0 displayName: Download ABI compatibility check tool from GitHub From 5238ba5d8f4987d0ccf79bf3f0fa065a31fead08 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 03:07:44 +0100 Subject: [PATCH 0032/6208] Fix tool extraction search pattern. --- .azure/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 3e133290c4..dcf69e2311 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -167,7 +167,7 @@ jobs: - task: ExtractFiles@1 displayName: Extract ABI compatibility check tool inputs: - archiveFilePatterns: '*-ci.zip' + archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip' destinationFolder: $(System.ArtifactsDirectory)/tools cleanDestinationFolder: true From 0849c4a4478581752197ae180643e6eaa3daaa1f Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 03:12:53 +0100 Subject: [PATCH 0033/6208] Switched to dotnet based execution again because of permissions. --- .azure/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index dcf69e2311..7437874bb5 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -174,7 +174,7 @@ jobs: - task: CmdLine@2 displayName: Execute ABI compatibility check tool inputs: - script: 'tools/CompatibilityCheckerCoreCLI current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)' + script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)' workingDirectory: $(System.ArtifactsDirectory) # Optional #failOnStderr: false # Optional From 029bafdf9c92c8ca09d10e2dff7c90dbd5c33f09 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 19 Feb 2019 03:16:17 +0100 Subject: [PATCH 0034/6208] Changed build name template and limited number of parallel jobs. --- .azure/azure-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index 7437874bb5..a14cf0aa3f 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -1,4 +1,4 @@ -name: $(Date:yyyyMMdd).$(Rev:.r) +name: $(Date:yyyyMMdd)$(Rev:.r) variables: - name: TestProjects @@ -26,6 +26,7 @@ jobs: BuildConfiguration: Release debug: BuildConfiguration: Debug + maxParallel: 2 steps: - checkout: self clean: false @@ -116,6 +117,7 @@ jobs: Common: NugetPackageName: Jellyfin.Common AssemblyFileName: MediaBrowser.Common.dll + maxParallel: 2 steps: - checkout: none From 6bbb968b578fe42224227b70e78825bbed5cfc6f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 20 Feb 2019 00:00:26 -0800 Subject: [PATCH 0035/6208] minor changes and return to netstandard --- .../Cryptography/CryptographyProvider.cs | 5 +- .../Data/SqliteUserRepository.cs | 3 +- .../Emby.Server.Implementations.csproj | 2 +- .../Library/DefaultAuthenticationProvider.cs | 33 ++++---- .../Library/UserManager.cs | 3 +- .../Cryptography/PasswordHash.cs | 81 +++++++++++-------- 6 files changed, 72 insertions(+), 55 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index dc528c280a..2f2fd95922 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -73,8 +73,9 @@ namespace Emby.Server.Implementations.Cryptography } private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) - { - using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations, new HashAlgorithmName(method))) + { + //downgrading for now as we need this library to be dotnetstandard compliant + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) { return r.GetBytes(32); } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index 1b6deae7d3..3df91f71cb 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Data if (!localUsersTableExists && TableExists(connection, "Users")) { TryMigrateToLocalUsersTable(connection); - } + } + RemoveEmptyPasswordHashes(); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 86b2efe54d..8356a9501b 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -34,7 +34,7 @@ - netcoreapp2.1 + netstandard2.0 false diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 80026d97c9..2ac3ef424e 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Library string CalculatedHashString; if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) { - if (String.IsNullOrEmpty(readyHash.Salt)) + if (string.IsNullOrEmpty(readyHash.Salt)) { CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); @@ -65,7 +65,8 @@ namespace Emby.Server.Implementations.Library { CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); - } + } + if (CalculatedHashString == readyHash.Hash) { success = true; @@ -95,18 +96,20 @@ namespace Emby.Server.Implementations.Library private void ConvertPasswordFormat(User user) { if (!string.IsNullOrEmpty(user.Password)) + { + return; + } + + if (!user.Password.Contains("$")) { - if (!user.Password.Contains("$")) - { - string hash = user.Password; - user.Password = String.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } + string hash = user.Password; + user.Password = String.Format("$SHA1${0}", hash); + } + + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = string.Format("$SHA1${0}", hash); } } @@ -122,6 +125,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(password); } + return false; } @@ -188,7 +192,8 @@ namespace Emby.Server.Implementations.Library { ConvertPasswordFormat(user); passwordHash = new PasswordHash(user.Password); - } + } + if (passwordHash.SaltBytes != null) { //the password is modern format with PBKDF and we should take advantage of that diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 3daed0c085..b74006233e 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -221,9 +221,8 @@ 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 - string UserNameRegex = "^[\\w-'._@]*$"; // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, UserNameRegex); + return Regex.IsMatch(username, "^[\\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 3a817543bc..49bd510e96 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -10,26 +10,33 @@ namespace MediaBrowser.Model.Cryptography //https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md //$[$=(,=)*][$[$]] - public string Id; - public Dictionary Parameters = new Dictionary(); - public string Salt; - public byte[] SaltBytes; - public string Hash; - public byte[] HashBytes; + private string id; + private Dictionary parameters = new Dictionary(); + private string salt; + private byte[] saltBytes; + private string hash; + private byte[] hashBytes; + public string Id { get => id; set => id = value; } + public Dictionary Parameters { get => parameters; set => parameters = value; } + public string Salt { get => salt; set => salt = value; } + public byte[] SaltBytes { get => saltBytes; set => saltBytes = value; } + public string Hash { get => hash; set => hash = value; } + public byte[] HashBytes { get => hashBytes; set => hashBytes = value; } + public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); - Id = splitted[1]; + id = splitted[1]; if (splitted[2].Contains("=")) { foreach (string paramset in (splitted[2].Split(','))) { - if (!String.IsNullOrEmpty(paramset)) + if (!string.IsNullOrEmpty(paramset)) { string[] fields = paramset.Split('='); if (fields.Length == 2) { - Parameters.Add(fields[0], fields[1]); + parameters.Add(fields[0], fields[1]); } else { @@ -39,32 +46,32 @@ namespace MediaBrowser.Model.Cryptography } if (splitted.Length == 5) { - Salt = splitted[3]; - SaltBytes = ConvertFromByteString(Salt); - Hash = splitted[4]; - HashBytes = ConvertFromByteString(Hash); + salt = splitted[3]; + saltBytes = ConvertFromByteString(salt); + hash = splitted[4]; + hashBytes = ConvertFromByteString(hash); } else { - Salt = string.Empty; - Hash = splitted[3]; - HashBytes = ConvertFromByteString(Hash); + salt = string.Empty; + hash = splitted[3]; + hashBytes = ConvertFromByteString(hash); } } else { if (splitted.Length == 4) { - Salt = splitted[2]; - SaltBytes = ConvertFromByteString(Salt); - Hash = splitted[3]; - HashBytes = ConvertFromByteString(Hash); + salt = splitted[2]; + saltBytes = ConvertFromByteString(salt); + hash = splitted[3]; + hashBytes = ConvertFromByteString(hash); } else { - Salt = string.Empty; - Hash = splitted[2]; - HashBytes = ConvertFromByteString(Hash); + salt = string.Empty; + hash = splitted[2]; + hashBytes = ConvertFromByteString(hash); } } @@ -73,9 +80,9 @@ namespace MediaBrowser.Model.Cryptography public PasswordHash(ICryptoProvider cryptoProvider) { - Id = cryptoProvider.DefaultHashMethod; - SaltBytes = cryptoProvider.GenerateSalt(); - Salt = ConvertToByteString(SaltBytes); + id = cryptoProvider.DefaultHashMethod; + saltBytes = cryptoProvider.GenerateSalt(); + salt = ConvertToByteString(SaltBytes); } public static byte[] ConvertFromByteString(string byteString) @@ -95,31 +102,35 @@ namespace MediaBrowser.Model.Cryptography private string SerializeParameters() { - string ReturnString = String.Empty; - foreach (var KVP in Parameters) + string ReturnString = string.Empty; + foreach (var KVP in parameters) { - ReturnString += String.Format(",{0}={1}", KVP.Key, KVP.Value); - } + ReturnString += $",{KVP.Key}={KVP.Value}"; + } + if ((!string.IsNullOrEmpty(ReturnString)) && ReturnString[0] == ',') { ReturnString = ReturnString.Remove(0, 1); - } + } + return ReturnString; } public override string ToString() { - string outString = "$" +Id; + string outString = "$" +id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { outString += $"${paramstring}"; } - if (!string.IsNullOrEmpty(Salt)) + + if (!string.IsNullOrEmpty(salt)) { - outString += $"${Salt}"; + outString += $"${salt}"; } - outString += $"${Hash}"; + + outString += $"${hash}"; return outString; } } From 098de6b0501eaa0375fc5bfd7cff369815b57718 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 20 Feb 2019 01:17:30 -0800 Subject: [PATCH 0036/6208] made newlines into linux newlines --- .../Cryptography/CryptographyProvider.cs | 294 +++++----- .../Data/SqliteUserRepository.cs | 526 +++++++++--------- .../Library/DefaultAuthenticationProvider.cs | 360 ++++++------ 3 files changed, 589 insertions(+), 591 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 2f2fd95922..ea719309cd 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,149 +1,149 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Cryptography -{ - public class CryptographyProvider : ICryptoProvider - { - private HashSet SupportedHashMethods; - public string DefaultHashMethod => "SHA256"; - private RandomNumberGenerator rng; - private int defaultiterations = 1000; - public CryptographyProvider() - { - //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 - //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one - SupportedHashMethods = new HashSet() - { - "MD5" - ,"System.Security.Cryptography.MD5" - ,"SHA" - ,"SHA1" - ,"System.Security.Cryptography.SHA1" - ,"SHA256" - ,"SHA-256" - ,"System.Security.Cryptography.SHA256" - ,"SHA384" - ,"SHA-384" - ,"System.Security.Cryptography.SHA384" - ,"SHA512" - ,"SHA-512" - ,"System.Security.Cryptography.SHA512" - }; - rng = RandomNumberGenerator.Create(); - } - - public Guid GetMD5(string str) - { - return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - } - - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } - - public IEnumerable GetSupportedHashMethods() - { - return SupportedHashMethods; - } - - private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) +using System.IO; +using System.Security.Cryptography; +using System.Text; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Cryptography +{ + public class CryptographyProvider : ICryptoProvider + { + private HashSet SupportedHashMethods; + public string DefaultHashMethod => "SHA256"; + private RandomNumberGenerator rng; + private int defaultiterations = 1000; + public CryptographyProvider() { - //downgrading for now as we need this library to be dotnetstandard compliant - using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) - { - return r.GetBytes(32); - } - } - - public byte[] ComputeHash(string HashMethod, byte[] bytes) - { - return ComputeHash(HashMethod, bytes, new byte[0]); - } - - public byte[] ComputeHashWithDefaultMethod(byte[] bytes) - { - return ComputeHash(DefaultHashMethod, bytes); - } - - public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) - { - if (SupportedHashMethods.Contains(HashMethod)) - { - if (salt.Length == 0) - { - using (var h = HashAlgorithm.Create(HashMethod)) - { - return h.ComputeHash(bytes); - } - } - else - { - return PBKDF2(HashMethod, bytes, salt, defaultiterations); - } - } - else - { - throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); - } - } - - public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - { - return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); - } - - public byte[] ComputeHash(PasswordHash hash) - { - int iterations = defaultiterations; - if (!hash.Parameters.ContainsKey("iterations")) - { - hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture)); - } - else - { - try - { - iterations = int.Parse(hash.Parameters["iterations"]); - } - catch (Exception e) - { - throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); - } - } - return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); - } - - public byte[] GenerateSalt() - { - byte[] salt = new byte[64]; - rng.GetBytes(salt); - return salt; - } - } -} + //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 + //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one + SupportedHashMethods = new HashSet() + { + "MD5" + ,"System.Security.Cryptography.MD5" + ,"SHA" + ,"SHA1" + ,"System.Security.Cryptography.SHA1" + ,"SHA256" + ,"SHA-256" + ,"System.Security.Cryptography.SHA256" + ,"SHA384" + ,"SHA-384" + ,"System.Security.Cryptography.SHA384" + ,"SHA512" + ,"SHA-512" + ,"System.Security.Cryptography.SHA512" + }; + rng = RandomNumberGenerator.Create(); + } + + public Guid GetMD5(string str) + { + return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); + } + + public byte[] ComputeSHA1(byte[] bytes) + { + using (var provider = SHA1.Create()) + { + return provider.ComputeHash(bytes); + } + } + + public byte[] ComputeMD5(Stream str) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(str); + } + } + + public byte[] ComputeMD5(byte[] bytes) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(bytes); + } + } + + public IEnumerable GetSupportedHashMethods() + { + return SupportedHashMethods; + } + + private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + { + //downgrading for now as we need this library to be dotnetstandard compliant + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) + { + return r.GetBytes(32); + } + } + + public byte[] ComputeHash(string HashMethod, byte[] bytes) + { + return ComputeHash(HashMethod, bytes, new byte[0]); + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes) + { + return ComputeHash(DefaultHashMethod, bytes); + } + + public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) + { + if (SupportedHashMethods.Contains(HashMethod)) + { + if (salt.Length == 0) + { + using (var h = HashAlgorithm.Create(HashMethod)) + { + return h.ComputeHash(bytes); + } + } + else + { + return PBKDF2(HashMethod, bytes, salt, defaultiterations); + } + } + else + { + throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); + } + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) + { + return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); + } + + public byte[] ComputeHash(PasswordHash hash) + { + int iterations = defaultiterations; + if (!hash.Parameters.ContainsKey("iterations")) + { + hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture)); + } + else + { + try + { + iterations = int.Parse(hash.Parameters["iterations"]); + } + catch (Exception e) + { + throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); + } + } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); + } + + public byte[] GenerateSalt() + { + byte[] salt = new byte[64]; + rng.GetBytes(salt); + return salt; + } + } +} diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index 3df91f71cb..182df0edc9 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -1,264 +1,264 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteUserRepository - /// - public class SqliteUserRepository : BaseSqliteRepository, IUserRepository - { - private readonly IJsonSerializer _jsonSerializer; - - public SqliteUserRepository( - ILoggerFactory loggerFactory, - IServerApplicationPaths appPaths, - IJsonSerializer jsonSerializer) - : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) - { - _jsonSerializer = jsonSerializer; - - DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name => "SQLite"; - - /// - /// Opens the connection to the database - /// - /// Task. - public void Initialize() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - var localUsersTableExists = TableExists(connection, "LocalUsersv2"); - - connection.RunQueries(new[] { - "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", - "drop index if exists idx_users" - }); - - if (!localUsersTableExists && TableExists(connection, "Users")) - { - TryMigrateToLocalUsersTable(connection); +using System; +using System.Collections.Generic; +using System.IO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteUserRepository + /// + public class SqliteUserRepository : BaseSqliteRepository, IUserRepository + { + private readonly IJsonSerializer _jsonSerializer; + + public SqliteUserRepository( + ILoggerFactory loggerFactory, + IServerApplicationPaths appPaths, + IJsonSerializer jsonSerializer) + : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) + { + _jsonSerializer = jsonSerializer; + + DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name => "SQLite"; + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + var localUsersTableExists = TableExists(connection, "LocalUsersv2"); + + connection.RunQueries(new[] { + "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", + "drop index if exists idx_users" + }); + + if (!localUsersTableExists && TableExists(connection, "Users")) + { + TryMigrateToLocalUsersTable(connection); } - - RemoveEmptyPasswordHashes(); - } - } - - private void TryMigrateToLocalUsersTable(ManagedConnection connection) - { - try - { - connection.RunQueries(new[] - { - "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating users database"); - } - } - - private void RemoveEmptyPasswordHashes() - { - foreach (var user in RetrieveAllUsers()) - { - // If the user password is the sha1 hash of the empty string, remove it - if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) - || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) - { - continue; - } - - user.Password = null; - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - - }, TransactionMode); - } - } - - } - - /// - /// Save a user in the repo - /// - public void CreateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) - { - statement.TryBind("@guid", user.Id.ToGuidBlob()); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - - var createdUser = GetUser(user.Id, false); - - if (createdUser == null) - { - throw new ApplicationException("created user should never be null"); - } - - user.InternalId = createdUser.InternalId; - - }, TransactionMode); - } - } - } - - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = _jsonSerializer.SerializeToBytes(user); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - - }, TransactionMode); - } - } - } - - private User GetUser(Guid guid, bool openLock) - { - using (openLock ? WriteLock.Read() : null) - { - using (var connection = CreateConnection(true)) - { - using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) - { - statement.TryBind("@guid", guid); - - foreach (var row in statement.ExecuteQuery()) - { - return GetUser(row); - } - } - } - } - - return null; - } - - private User GetUser(IReadOnlyList row) - { - var id = row[0].ToInt64(); - var guid = row[1].ReadGuidFromBlob(); - - using (var stream = new MemoryStream(row[2].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream(stream); - user.InternalId = id; - user.Id = guid; - return user; - } - } - - /// - /// Retrieve all users from the database - /// - /// IEnumerable{User}. - public List RetrieveAllUsers() - { - var list = new List(); - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) - { - list.Add(GetUser(row)); - } - } - } - - return list; - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - public void DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) - { - statement.TryBind("@id", user.InternalId); - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - } -} + + RemoveEmptyPasswordHashes(); + } + } + + private void TryMigrateToLocalUsersTable(ManagedConnection connection) + { + try + { + connection.RunQueries(new[] + { + "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" + }); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error migrating users database"); + } + } + + private void RemoveEmptyPasswordHashes() + { + foreach (var user in RetrieveAllUsers()) + { + // If the user password is the sha1 hash of the empty string, remove it + if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) + || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) + { + continue; + } + + user.Password = null; + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + + } + + /// + /// Save a user in the repo + /// + public void CreateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) + { + statement.TryBind("@guid", user.Id.ToGuidBlob()); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } + + var createdUser = GetUser(user.Id, false); + + if (createdUser == null) + { + throw new ApplicationException("created user should never be null"); + } + + user.InternalId = createdUser.InternalId; + + }, TransactionMode); + } + } + } + + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + } + + private User GetUser(Guid guid, bool openLock) + { + using (openLock ? WriteLock.Read() : null) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) + { + statement.TryBind("@guid", guid); + + foreach (var row in statement.ExecuteQuery()) + { + return GetUser(row); + } + } + } + } + + return null; + } + + private User GetUser(IReadOnlyList row) + { + var id = row[0].ToInt64(); + var guid = row[1].ReadGuidFromBlob(); + + using (var stream = new MemoryStream(row[2].ToBlob())) + { + stream.Position = 0; + var user = _jsonSerializer.DeserializeFromStream(stream); + user.InternalId = id; + user.Id = guid; + return user; + } + } + + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + public List RetrieveAllUsers() + { + var list = new List(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) + { + list.Add(GetUser(row)); + } + } + } + + return list; + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + public void DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) + { + statement.TryBind("@id", user.InternalId); + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + } +} diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 2ac3ef424e..b58374adb4 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,42 +1,42 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - public DefaultAuthenticationProvider(ICryptoProvider crypto) - { - _cryptographyProvider = crypto; - } - - 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 - public Task Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - - //This is the verson that we need to use for local users. Because reasons. - public Task Authenticate(string username, string password, User resolvedUser) - { - bool success = false; - if (resolvedUser == null) - { - throw new Exception("Invalid username or password"); +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider crypto) + { + _cryptographyProvider = crypto; + } + + 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 + public Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + + //This is the verson that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, User resolvedUser) + { + bool success = false; + if (resolvedUser == null) + { + throw new Exception("Invalid username or password"); } //As long as jellyfin supports passwordless users, we need this little block here to accomodate @@ -47,166 +47,164 @@ namespace Emby.Server.Implementations.Library Username = username }); } - - ConvertPasswordFormat(resolvedUser); - byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] CalculatedHash; - string CalculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) - { - if (string.IsNullOrEmpty(readyHash.Salt)) - { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); - } - else - { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + + ConvertPasswordFormat(resolvedUser); + byte[] passwordbytes = Encoding.UTF8.GetBytes(password); + + PasswordHash readyHash = new PasswordHash(resolvedUser.Password); + byte[] CalculatedHash; + string CalculatedHashString; + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) + { + if (string.IsNullOrEmpty(readyHash.Salt)) + { + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } - - if (CalculatedHashString == readyHash.Hash) - { - success = true; - //throw new Exception("Invalid username or password"); - } - } - else - { - throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); - } - - //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - - if (!success) - { - throw new Exception("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - //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)) + else + { + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + } + + if (CalculatedHashString == readyHash.Hash) + { + success = true; + //throw new Exception("Invalid username or password"); + } + } + else + { + throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); + } + + //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + //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.Contains("$")) - { - string hash = user.Password; - user.Password = String.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } - } - - public Task HasPassword(User user) - { - var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); - return Task.FromResult(hasConfiguredPassword); - } - - private bool IsPasswordEmpty(User user, string password) + + if (!user.Password.Contains("$")) + { + string hash = user.Password; + user.Password = String.Format("$SHA1${0}", hash); + } + + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = string.Format("$SHA1${0}", hash); + } + } + + public Task HasPassword(User user) + { + var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); + return Task.FromResult(hasConfiguredPassword); + } + + private bool IsPasswordEmpty(User user, string password) { if (string.IsNullOrEmpty(user.Password)) { return string.IsNullOrEmpty(password); } - return false; - } - - public Task ChangePassword(User user, string newPassword) - { + return false; + } + + 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)) { PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); - newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); - newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; + newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); + newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); user.Password = newPasswordHash.ToString(); return Task.CompletedTask; } - PasswordHash passwordHash = new PasswordHash(user.Password); - if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) - { - passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); - passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); - } - else if (newPassword != null) - { - passwordHash.Hash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(passwordHash.Hash)) - { - throw new ArgumentNullException(nameof(passwordHash.Hash)); - } - - user.Password = passwordHash.ToString(); - - return Task.CompletedTask; - } - - public string GetPasswordHash(User user) - { - return user.Password; - } - - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) - { - passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); - } - - /// - /// Gets the hashed string. - /// - 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); + PasswordHash passwordHash = new PasswordHash(user.Password); + if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) + { + passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); + passwordHash.Id = _cryptographyProvider.DefaultHashMethod; + passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); } - - if (passwordHash.SaltBytes != null) - { - //the password is modern format with PBKDF and we should take advantage of that - passwordHash.HashBytes = 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))); - } - - - } - } -} + else if (newPassword != null) + { + passwordHash.Hash = GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(passwordHash.Hash)) + { + throw new ArgumentNullException(nameof(passwordHash.Hash)); + } + + user.Password = passwordHash.ToString(); + + return Task.CompletedTask; + } + + public string GetPasswordHash(User user) + { + return user.Password; + } + + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) + { + passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); + } + + /// + /// Gets the hashed string. + /// + 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); + } + + if (passwordHash.SaltBytes != null) + { + //the password is modern format with PBKDF and we should take advantage of that + passwordHash.HashBytes = 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))); + } + } + } +} From a23f04623ed2738ab1205717674614e9eed6b548 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 16 Feb 2019 12:39:53 +0100 Subject: [PATCH 0037/6208] Remove IEncryptionManager --- .../ApplicationHost.cs | 4 +- .../Security/EncryptionManager.cs | 57 ------------------- .../Security/IEncryptionManager.cs | 19 ------- .../Subtitles/OpenSubtitleDownloader.cs | 19 ++++--- 4 files changed, 11 insertions(+), 88 deletions(-) delete mode 100644 Emby.Server.Implementations/Security/EncryptionManager.cs delete mode 100644 MediaBrowser.Controller/Security/IEncryptionManager.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8daba05850..dcf2098d49 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -739,10 +739,8 @@ namespace Emby.Server.Implementations TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); serviceCollection.AddSingleton(TVSeriesManager); - var encryptionManager = new EncryptionManager(); - serviceCollection.AddSingleton(encryptionManager); - DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager); + serviceCollection.AddSingleton(DeviceManager); MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder); diff --git a/Emby.Server.Implementations/Security/EncryptionManager.cs b/Emby.Server.Implementations/Security/EncryptionManager.cs deleted file mode 100644 index fa8872ccc3..0000000000 --- a/Emby.Server.Implementations/Security/EncryptionManager.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Text; -using MediaBrowser.Controller.Security; - -namespace Emby.Server.Implementations.Security -{ - public class EncryptionManager : IEncryptionManager - { - /// - /// Encrypts the string. - /// - /// The value. - /// System.String. - /// value - public string EncryptString(string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return EncryptStringUniversal(value); - } - - /// - /// Decrypts the string. - /// - /// The value. - /// System.String. - /// value - public string DecryptString(string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return DecryptStringUniversal(value); - } - - private static string EncryptStringUniversal(string value) - { - // Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now - - var bytes = Encoding.UTF8.GetBytes(value); - return Convert.ToBase64String(bytes); - } - - private static string DecryptStringUniversal(string value) - { - // Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now - - var bytes = Convert.FromBase64String(value); - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); - } - } -} diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs deleted file mode 100644 index 68680fdf3f..0000000000 --- a/MediaBrowser.Controller/Security/IEncryptionManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MediaBrowser.Controller.Security -{ - public interface IEncryptionManager - { - /// - /// Encrypts the string. - /// - /// The value. - /// System.String. - string EncryptString(string value); - - /// - /// Decrypts the string. - /// - /// The value. - /// System.String. - string DecryptString(string value); - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs index 6a5162b8d6..c76ff3feda 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IServerConfigurationManager _config; - private readonly IEncryptionManager _encryption; private readonly IJsonSerializer _json; private readonly IFileSystem _fileSystem; - public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem) + public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem) { _logger = loggerFactory.CreateLogger(GetType().Name); _httpClient = httpClient; _config = config; - _encryption = encryption; _json = json; _fileSystem = fileSystem; @@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) && !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) { - options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash); + options.OpenSubtitlesPasswordHash = ToBase64EncodedString(options.OpenSubtitlesPasswordHash); } } - private string EncryptPassword(string password) + private static string ToBase64EncodedString(string password) { - return PasswordHashPrefix + _encryption.EncryptString(password); + var bytes = Encoding.UTF8.GetBytes(password); + return PasswordHashPrefix + bytes; } - private string DecryptPassword(string password) + private static string DecodeBase64EncodedString(string password) { if (password == null || !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) @@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles return string.Empty; } - return _encryption.DecryptString(password.Substring(2)); + var bytes = Convert.FromBase64String(password.Substring(2)); + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); } public string Name => "Open Subtitles"; @@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var options = GetOptions(); var user = options.OpenSubtitlesUsername ?? string.Empty; - var password = DecryptPassword(options.OpenSubtitlesPasswordHash); + var password = DecodeBase64EncodedString(options.OpenSubtitlesPasswordHash); var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false); From 1e2050f1065551222bbd053473830b2c1a950793 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 16 Feb 2019 12:44:17 +0100 Subject: [PATCH 0038/6208] Rename functions to match functionality --- .../Subtitles/OpenSubtitleDownloader.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs index c76ff3feda..dde4ee7add 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs @@ -62,17 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) && !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) { - options.OpenSubtitlesPasswordHash = ToBase64EncodedString(options.OpenSubtitlesPasswordHash); + options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash); } } - private static string ToBase64EncodedString(string password) + private static string EncodePassword(string password) { var bytes = Encoding.UTF8.GetBytes(password); return PasswordHashPrefix + bytes; } - private static string DecodeBase64EncodedString(string password) + private static string DecodePassword(string password) { if (password == null || !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) @@ -187,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var options = GetOptions(); var user = options.OpenSubtitlesUsername ?? string.Empty; - var password = DecodeBase64EncodedString(options.OpenSubtitlesPasswordHash); + var password = DecodePassword(options.OpenSubtitlesPasswordHash); var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false); From 139807719c9f7a0176eeacf7a64c95dc070850bc Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 16 Feb 2019 17:21:42 +0100 Subject: [PATCH 0039/6208] Add missing base64 conversion --- MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs index dde4ee7add..a7e3f61972 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private static string EncodePassword(string password) { var bytes = Encoding.UTF8.GetBytes(password); - return PasswordHashPrefix + bytes; + return PasswordHashPrefix + Convert.ToBase64String(bytes); } private static string DecodePassword(string password) From 320707d44c403b167f6365299054b1a86a9d15cf Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 20 Feb 2019 16:49:03 +0100 Subject: [PATCH 0040/6208] Reduce string allocations at startup --- .../Services/ServicePath.cs | 161 +++++++----------- .../Services/StringMapTypeDeserializer.cs | 52 +++--- 2 files changed, 91 insertions(+), 122 deletions(-) diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index f575baca30..ccb28e8df0 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services private const char ComponentSeperator = '.'; private const string VariablePrefix = "{"; - readonly bool[] componentsWithSeparators; + private readonly bool[] componentsWithSeparators; private readonly string restPath; public bool IsWildCardPath { get; private set; } @@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services public string Description { get; private set; } public bool IsHidden { get; private set; } - public int Priority { get; set; } //passed back to RouteAttribute - - public IEnumerable PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e)); - public static string[] GetPathPartsForMatching(string pathInfo) { return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); @@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services { list.Add(hashPrefix + part); - var subParts = part.Split(ComponentSeperator); - if (subParts.Length == 1) continue; + if (part.IndexOf(ComponentSeperator) == -1) + { + continue; + } + var subParts = part.Split(ComponentSeperator); foreach (var subPart in subParts) { list.Add(hashPrefix + subPart); @@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services { if (string.IsNullOrEmpty(component)) continue; - if (StringContains(component, VariablePrefix) + if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 && component.IndexOf(ComponentSeperator) != -1) { hasSeparators.Add(true); @@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services for (var i = 0; i < components.Length - 1; i++) { - if (!this.isWildcard[i]) continue; + if (!this.isWildcard[i]) + { + continue; + } + if (this.literalsToMatch[i + 1] == null) { throw new ArgumentException( @@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services } } - this.wildcardCount = this.isWildcard.Count(x => x); + this.wildcardCount = this.isWildcard.Length; this.IsWildCardPath = this.wildcardCount > 0; this.FirstMatchHashKey = !this.IsWildCardPath @@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services : WildCardChar + PathSeperator + firstLiteralMatch; this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - RegisterCaseInsenstivePropertyNameMappings(); + + _propertyNamesMap = new HashSet( + GetSerializableProperties(RequestType).Select(x => x.Name), + StringComparer.OrdinalIgnoreCase); } - private void RegisterCaseInsenstivePropertyNameMappings() + internal static string[] IgnoreAttributesNamed = new[] { - foreach (var propertyInfo in GetSerializableProperties(RequestType)) - { - var propertyName = propertyInfo.Name; - propertyNamesMap.Add(propertyName.ToLowerInvariant(), propertyName); - } - } - - internal static string[] IgnoreAttributesNamed = new[] { "IgnoreDataMemberAttribute", "JsonIgnoreAttribute" }; @@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services private static Type excludeType = typeof(Stream); - internal static List GetSerializableProperties(Type type) + internal static IEnumerable GetSerializableProperties(Type type) { - var list = new List(); - var props = GetPublicProperties(type); - - foreach (var prop in props) + foreach (var prop in GetPublicProperties(type)) { - if (prop.GetMethod == null) - { - continue; - } - - if (excludeType == prop.PropertyType) + if (prop.GetMethod == null + || excludeType == prop.PropertyType) { continue; } @@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services if (!ignored) { - list.Add(prop); + yield return prop; } } - - // else return those properties that are not decorated with IgnoreDataMember - return list; } - private static List GetPublicProperties(Type type) + private static IEnumerable GetPublicProperties(Type type) { - if (type.GetTypeInfo().IsInterface) + if (type.IsInterface) { var propertyInfos = new List(); - - var considered = new List(); + var considered = new List() + { + type + }; var queue = new Queue(); - considered.Add(type); queue.Enqueue(type); while (queue.Count > 0) @@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services var subType = queue.Dequeue(); foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) { - if (considered.Contains(subInterface)) continue; + if (considered.Contains(subInterface)) + { + continue; + } considered.Add(subInterface); queue.Enqueue(subInterface); } - var typeProperties = GetTypesPublicProperties(subType); - - var newPropertyInfos = typeProperties + var newPropertyInfos = GetTypesPublicProperties(subType) .Where(x => !propertyInfos.Contains(x)); propertyInfos.InsertRange(0, newPropertyInfos); @@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services return propertyInfos; } - var list = new List(); - - foreach (var t in GetTypesPublicProperties(type)) - { - if (t.GetIndexParameters().Length == 0) - { - list.Add(t); - } - } - return list; + return GetTypesPublicProperties(type) + .Where(x => x.GetIndexParameters().Length == 0); } - private static PropertyInfo[] GetTypesPublicProperties(Type subType) + private static IEnumerable GetTypesPublicProperties(Type subType) { - var pis = new List(); foreach (var pi in subType.GetRuntimeProperties()) { var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) continue; - pis.Add(pi); + if (mi != null && mi.IsStatic) + { + continue; + } + + yield return pi; } - return pis.ToArray(); } /// @@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services private readonly StringMapTypeDeserializer typeDeserializer; - private readonly Dictionary propertyNamesMap = new Dictionary(); + private readonly HashSet _propertyNamesMap; public int MatchScore(string httpMethod, string[] withPathInfoParts) { @@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services return -1; } - var score = 0; - //Routes with least wildcard matches get the highest score - score += Math.Max((100 - wildcardMatchCount), 1) * 1000; - - //Routes with less variable (and more literal) matches - score += Math.Max((10 - VariableArgsCount), 1) * 100; + var score = Math.Max((100 - wildcardMatchCount), 1) * 1000 + //Routes with less variable (and more literal) matches + + Math.Max((10 - VariableArgsCount), 1) * 100; //Exact verb match is better than ANY if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) @@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services return score; } - private bool StringContains(string str1, string str2) - { - return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1; - } - /// /// For performance withPathInfoParts should already be a lower case string /// to minimize redundant matching operations. @@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services if (i < this.TotalComponentsCount - 1) { // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1])) + while (pathIx < withPathInfoParts.Length + && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) { pathIx++; wildcardMatchCount++; @@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services continue; } - if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch)) + if (withPathInfoParts.Length <= pathIx + || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) { return false; } + pathIx++; } } @@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services return pathIx == withPathInfoParts.Length; } - private static bool LiteralsEqual(string str1, string str2) - { - // Most cases - if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Handle turkish i - str1 = str1.ToUpperInvariant(); - str2 = str2.ToUpperInvariant(); - - // Invariant IgnoreCase would probably be better but it's not available in PCL - return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase); - } - private bool ExplodeComponents(ref string[] withPathInfoParts) { var totalComponents = new List(); for (var i = 0; i < withPathInfoParts.Length; i++) { var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) continue; + if (string.IsNullOrEmpty(component)) + { + continue; + } if (this.PathComponentsCount != this.TotalComponentsCount && this.componentsWithSeparators[i]) { var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) return false; + if (subComponents.Length < 2) + { + return false; + } + totalComponents.AddRange(subComponents); } else @@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services continue; } - if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest)) + if (!this._propertyNamesMap.Contains(variableName)) { if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) { @@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services { sb.Append(PathSeperatorChar + requestComponents[j]); } + value = sb.ToString(); } else @@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) { - var sb = new StringBuilder(); - sb.Append(value); + var sb = new StringBuilder(value); pathIx++; while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) { sb.Append(PathSeperatorChar + requestComponents[pathIx++]); } + value = sb.ToString(); } else @@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services pathIx++; } - requestKeyValuesMap[propertyNameOnRequest] = value; + requestKeyValuesMap[variableName] = value; } if (queryStringAndFormData != null) diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index d13935fbab..f835aa1b5b 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services { internal class PropertySerializerEntry { - public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn) + public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn, Type propertyType) { PropertySetFn = propertySetFn; PropertyParseStringFn = propertyParseStringFn; + PropertyType = PropertyType; } - public Action PropertySetFn; - public Func PropertyParseStringFn; - public Type PropertyType; + public Action PropertySetFn { get; private set; } + public Func PropertyParseStringFn { get; private set; } + public Type PropertyType { get; private set; } } private readonly Type type; @@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services public Func GetParseFn(Type propertyType) { if (propertyType == typeof(string)) + { return s => s; + } return _GetParseFn(propertyType); } @@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); var propertyType = propertyInfo.PropertyType; var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); propertySetterMap[propertyInfo.Name] = propertySerializer; } @@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services public object PopulateFromMap(object instance, IDictionary keyValuePairs) { - string propertyName = null; - string propertyTextValue = null; PropertySerializerEntry propertySerializerEntry = null; if (instance == null) + { instance = _CreateInstanceFn(type); + } foreach (var pair in keyValuePairs) { - propertyName = pair.Key; - propertyTextValue = pair.Value; + string propertyName = pair.Key; + string propertyTextValue = pair.Value; - if (string.IsNullOrEmpty(propertyTextValue)) - { - continue; - } - - if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) - { - if (propertyName == "v") - { - continue; - } - - continue; - } - - if (propertySerializerEntry.PropertySetFn == null) + if (string.IsNullOrEmpty(propertyTextValue) + || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) + || propertySerializerEntry.PropertySetFn == null) { continue; } @@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services { continue; } + propertySerializerEntry.PropertySetFn(instance, value); } @@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services public static string LeftPart(string strVal, char needle) { - if (strVal == null) return null; + if (strVal == null) + { + return null; + } + var pos = strVal.IndexOf(needle); return pos == -1 ? strVal @@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services { public static Action GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null; + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) + { + return null; + } var setMethodInfo = propertyInfo.SetMethod; return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); From 469a17b3ca6e4e9a032cfe66010e65cc66d27f33 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 20 Feb 2019 20:36:49 -0500 Subject: [PATCH 0041/6208] Install the dotnet runtime too This is needed since /usr/bin/dotnet doesn't exist in the SDK package for whatever reason as of Feb 18 2019. --- deployment/fedora-package-x64/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile index 8bb1d527da..397c944eae 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/fedora-package-x64/Dockerfile @@ -13,7 +13,7 @@ RUN dnf update -y \ && dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \ && dnf copr enable -y @dotnet-sig/dotnet \ && rpmdev-setuptree \ - && dnf install -y dotnet-sdk-${SDK_VERSION} \ + && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ From 1bc2b12ee3cdb6180cd6159840255fa32d23824f Mon Sep 17 00:00:00 2001 From: "Brian J. Murrell" Date: Thu, 21 Feb 2019 01:30:51 -0500 Subject: [PATCH 0042/6208] dotnet-runtime is needed in Fedora RPM build also --- deployment/fedora-package-x64/pkg-src/jellyfin.spec | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 146486428e..451a304b76 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -8,7 +8,7 @@ Name: jellyfin Version: 10.2.1 -Release: 1%{?dist} +Release: 2%{?dist} Summary: The Free Software Media Browser License: GPLv2 URL: https://jellyfin.media @@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, Requires: libcurl, fontconfig, freetype, openssl, glibc libicu # Requirements not packaged in main repos # COPR @dotnet-sig/dotnet -BuildRequires: dotnet-sdk-2.2 +BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2 # RPMfusion free Requires: ffmpeg @@ -140,6 +140,9 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Thu Feb 21 2019 Brian J. Murrell +- jellyfin: +- dotnet seems to have moved to dotnet-runtime * Wed Feb 20 2019 Jellyfin Packaging Team - jellyfin: - PR920 Fix cachedir missing from Docker container From f03e279382e9767d8d2702ad69a8e729a85d26f1 Mon Sep 17 00:00:00 2001 From: "Brian J. Murrell" Date: Thu, 14 Feb 2019 22:07:46 -0500 Subject: [PATCH 0043/6208] COPR auto building This adds enhancements so that Fedora/EL packages can be automatically built in COPR when a webhook is received. A typical webhook could be for tagging events for example or even a "Release" webhook to only build releases. --- .copr/Makefile | 8 +++ .../fedora-package-x64/create_tarball.sh | 55 +++++++++++++++++++ deployment/fedora-package-x64/package.sh | 47 +--------------- .../fedora-package-x64/pkg-src/jellyfin.spec | 1 + 4 files changed, 65 insertions(+), 46 deletions(-) create mode 100644 .copr/Makefile create mode 100755 deployment/fedora-package-x64/create_tarball.sh diff --git a/.copr/Makefile b/.copr/Makefile new file mode 100644 index 0000000000..84b98a0116 --- /dev/null +++ b/.copr/Makefile @@ -0,0 +1,8 @@ +srpm: + dnf -y install git + git submodule update --init --recursive + cd deployment/fedora-package-x64; \ + ./create_tarball.sh; \ + rpmbuild -bs pkg-src/jellyfin.spec \ + --define "_sourcedir $$PWD/pkg-src/" \ + --define "_srcrpmdir $(outdir)" diff --git a/deployment/fedora-package-x64/create_tarball.sh b/deployment/fedora-package-x64/create_tarball.sh new file mode 100755 index 0000000000..e8301c9897 --- /dev/null +++ b/deployment/fedora-package-x64/create_tarball.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source ../common.build.sh + +WORKDIR="$( pwd )" +VERSION="$( sed -ne '/^Version:/s/.* *//p' "${WORKDIR}"/pkg-src/jellyfin.spec )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +pkg_src_dir="${WORKDIR}/pkg-src" + +GNU_TAR=1 +echo "Bundling all sources for RPM build." +tar \ +--transform "s,^\.,jellyfin-${VERSION}," \ +--exclude='.git*' \ +--exclude='**/.git' \ +--exclude='**/.hg' \ +--exclude='**/.vs' \ +--exclude='**/.vscode' \ +--exclude='deployment' \ +--exclude='**/bin' \ +--exclude='**/obj' \ +--exclude='**/.nuget' \ +--exclude='*.deb' \ +--exclude='*.rpm' \ +-czf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz" \ +-C "../.." ./ || GNU_TAR=0 + +if [ $GNU_TAR -eq 0 ]; then + echo "The installed tar binary did not support --transform. Using workaround." + mkdir -p "${package_temporary_dir}/jellyfin"{,-"${VERSION}"} + # Not GNU tar + tar \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -zcf \ + "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ + -C "../.." ./ + echo "Extracting filtered package." + tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" + echo "Removing filtered package." + rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" + echo "Repackaging package into final tarball." + tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" +fi diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh index 74586417d9..eed29aef3c 100755 --- a/deployment/fedora-package-x64/package.sh +++ b/deployment/fedora-package-x64/package.sh @@ -21,52 +21,7 @@ else docker_sudo="" fi -# Create RPM source archive -GNU_TAR=1 -mkdir -p "${package_temporary_dir}" -echo "Bundling all sources for RPM build." -tar \ ---transform "s,^\.,jellyfin-${VERSION}," \ ---exclude='.git*' \ ---exclude='**/.git' \ ---exclude='**/.hg' \ ---exclude='**/.vs' \ ---exclude='**/.vscode' \ ---exclude='deployment' \ ---exclude='**/bin' \ ---exclude='**/obj' \ ---exclude='**/.nuget' \ ---exclude='*.deb' \ ---exclude='*.rpm' \ --czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" \ --C "../.." ./ || GNU_TAR=0 - -if [ $GNU_TAR -eq 0 ]; then - echo "The installed tar binary did not support --transform. Using workaround." - mkdir -p "${package_temporary_dir}/jellyfin" - # Not GNU tar - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -zcf \ - "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ - -C "../.." ./ - echo "Extracting filtered package." - tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" - echo "Removing filtered package." - rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" - echo "Repackaging package into final tarball." - tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" -fi +./create_tarball.sh # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 451a304b76..cfe4212a05 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -143,6 +143,7 @@ fi * Thu Feb 21 2019 Brian J. Murrell - jellyfin: - dotnet seems to have moved to dotnet-runtime +- COPR auto-build * Wed Feb 20 2019 Jellyfin Packaging Team - jellyfin: - PR920 Fix cachedir missing from Docker container From 00fe66a38c3b134b4516009c1ba0fd1cb5c38dbf Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 21 Feb 2019 20:59:10 +0100 Subject: [PATCH 0044/6208] Update submodule --- .gitmodules | 1 + MediaBrowser.WebDashboard/jellyfin-web | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index c10f5905c4..2b97b13311 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "MediaBrowser.WebDashboard/jellyfin-web"] path = MediaBrowser.WebDashboard/jellyfin-web url = https://github.com/jellyfin/jellyfin-web.git + branch = . diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index f7e5946c79..ec5a3b6e5e 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit f7e5946c79728c6ac8956c9dd4305afa4190402c +Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d From 9b39404b9a178e262fb50a2718b9daedb93f71f3 Mon Sep 17 00:00:00 2001 From: Lynxy Date: Thu, 21 Feb 2019 19:50:57 -0500 Subject: [PATCH 0045/6208] Always set ffmpeg flag +genpts when video stream is being copied --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f5f147db1b..264a45d24a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding { flags.Add("+ignidx"); } - if (state.GenPtsInput) + if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { flags.Add("+genpts"); } @@ -2436,6 +2436,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { + args += " -flags -global_header -fflags +genpts"; + if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) @@ -2447,11 +2449,6 @@ namespace MediaBrowser.Controller.MediaEncoding { args += " -copyts -avoid_negative_ts disabled -start_at_zero"; } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header -fflags +genpts"; - } } else { From cf4e64f4309a40ff50607d418970bc2767a275bb Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Thu, 21 Feb 2019 22:35:31 +0800 Subject: [PATCH 0046/6208] Add option to toggle if ignore virtual interfaces Some VPN like ZerotierOne owns IP address but no gateway, and there is no good idea in NetworkManager.GetIPsDefault() to filter such virtual interfaces, so just provide one option to let user decide it. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Networking/NetworkManager.cs | 12 ++++++------ MediaBrowser.Common/Net/INetworkManager.cs | 2 +- .../Configuration/ServerConfiguration.cs | 2 ++ RSSDP/RSSDP.csproj | 1 + RSSDP/SsdpCommunicationsServer.cs | 8 ++++++-- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a200065782..4eb4cde81c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -171,7 +171,7 @@ namespace Emby.Dlna.Main { var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) { IsShared = true }; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 042b04b3bb..e9f43a0eae 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1577,7 +1577,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(NetworkManager.GetLocalIpAddresses()); + addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); } var resultList = new List(); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 60cc9b88eb..b7a125f207 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking private IpAddressInfo[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - public IpAddressInfo[] GetLocalIpAddresses() + public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray(); + var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray(); _localIpAddresses = addresses; @@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking } } - private async Task> GetLocalIpAddressesInternal() + private async Task> GetLocalIpAddressesInternal(bool ignoreVirtualInterface) { - var list = GetIPsDefault() + var list = GetIPsDefault(ignoreVirtualInterface) .ToList(); if (list.Count == 0) @@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking return Dns.GetHostAddressesAsync(hostName); } - private List GetIPsDefault() + private List GetIPsDefault(bool ignoreVirtualInterface) { NetworkInterface[] interfaces; @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking // Try to exclude virtual adapters // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms var addr = ipProperties.GatewayAddresses.FirstOrDefault(); - if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase)) + if (addr == null || (ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))) { return new List(); } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 72fb6e2b86..3364230d1d 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); - IpAddressInfo[] GetLocalIpAddresses(); + IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface); IpAddressInfo ParseIpAddress(string ipAddress); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index ed58003292..0ba36b4b9d 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration public string[] LocalNetworkSubnets { get; set; } public string[] LocalNetworkAddresses { get; set; } public string[] CodecsUsed { get; set; } + public bool IgnoreVirtualInterfaces { get; set; } public bool EnableExternalContentInSuggestions { get; set; } public bool RequireHttps { get; set; } public bool IsBehindProxy { get; set; } @@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration CodecsUsed = Array.Empty(); ImageExtractionTimeoutMs = 0; PathSubstitutions = Array.Empty(); + IgnoreVirtualInterfaces = false; EnableSimpleArtistDetection = true; DisplaySpecialsWithinSeasons = true; diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index f06d4687b9..456a93aa80 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -3,6 +3,7 @@ + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 04e76ef591..9da906b496 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Configuration; namespace Rssdp.Infrastructure { @@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure private readonly ILogger _logger; private ISocketFactory _SocketFactory; private readonly INetworkManager _networkManager; + private readonly IServerConfigurationManager _config; private int _LocalPort; private int _MulticastTtl; @@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// /// The argument is null. - public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory, + INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) { + _config = config; } /// @@ -363,7 +367,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces)) { if (address.AddressFamily == IpAddressFamily.InterNetworkV6) { From 2db1826ed887bb84dc5065ff38ae67fe92cc6c51 Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Fri, 22 Feb 2019 10:13:52 +0800 Subject: [PATCH 0047/6208] Enable DLNA multi socket binding for linux If not, DLNA on multiple interfaces not works for linux, for example ZerotierOne VPN. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 4eb4cde81c..8eff7f773d 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -169,7 +169,8 @@ namespace Emby.Dlna.Main { if (_communicationsServer == null) { - var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows || + _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux; _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) { From cbd0e71c077e6233bcbc751f9a2f1ee742000ba3 Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Fri, 22 Feb 2019 12:06:49 +0800 Subject: [PATCH 0048/6208] Send DLNA devices message to only the matched interface This will be the right way for multiple interfaces, or the client will receive all devices message with different IP addresses and could not detect which one could access. And provide one option DlnaOptions.SendOnlyMatchedHost to fallback to old behaviour if this commit missed something. --- Emby.Dlna/Configuration/DlnaOptions.cs | 2 + Emby.Dlna/Main/DlnaEntryPoint.cs | 4 +- .../Networking/NetworkManager.cs | 60 +++++++++++++++++++ MediaBrowser.Common/Net/INetworkManager.cs | 3 + MediaBrowser.Model/Net/IpAddressInfo.cs | 1 + RSSDP/ISsdpCommunicationsServer.cs | 6 +- RSSDP/SsdpCommunicationsServer.cs | 13 ++-- RSSDP/SsdpDeviceLocator.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 30 ++++++++-- RSSDP/SsdpRootDevice.cs | 10 ++++ 10 files changed, 116 insertions(+), 15 deletions(-) diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index 0ebb490a1f..c7cb364a83 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration public bool EnableServer { get; set; } public bool EnableDebugLog { get; set; } public bool BlastAliveMessages { get; set; } + public bool SendOnlyMatchedHost { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; } public int BlastAliveMessageIntervalSeconds { get; set; } public string DefaultUserId { get; set; } @@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration EnablePlayTo = true; EnableServer = true; BlastAliveMessages = true; + SendOnlyMatchedHost = true; ClientDiscoveryIntervalSeconds = 60; BlastAliveMessageIntervalSeconds = 1800; } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 8eff7f773d..427b3a5c0e 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -230,7 +230,7 @@ namespace Emby.Dlna.Main try { - _Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion); + _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _Publisher.LogFunction = LogMessage; _Publisher.SupportPnpRootDevice = false; @@ -269,6 +269,8 @@ namespace Emby.Dlna.Main { CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info. Location = uri, // Must point to the URL that serves your devices UPnP description document. + Address = address, + SubnetMask = _networkManager.GetLocalIpSubnetMask(address), FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index b7a125f207..8696d18968 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking return false; } + public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask) + { + IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask)); + IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask)); + return network1.Equals(network2); + } + + private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) + { + byte[] ipAdressBytes = address.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + { + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + } + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) + { + broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i])); + } + return new IPAddress(broadcastAddress); + } + + public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address) + { + NetworkInterface[] interfaces; + IPAddress ipaddress = ToIPAddress(address); + + try + { + var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; + + interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => validStatuses.Contains(i.OperationalStatus)) + .ToArray(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in GetAllNetworkInterfaces"); + return null; + } + + foreach (NetworkInterface ni in interfaces) + { + if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) + { + foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) + { + if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null) + { + return ToIpAddressInfo(ip.IPv4Mask); + } + } + } + } + return null; + } + public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) { if (endpoint == null) diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 3364230d1d..34c6f58665 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net Task GetHostAddressesAsync(string host); bool IsAddressInSubnets(string addressString, string[] subnets); + + bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask); + IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address); } } diff --git a/MediaBrowser.Model/Net/IpAddressInfo.cs b/MediaBrowser.Model/Net/IpAddressInfo.cs index 7a278d4d41..87fa55bcae 100644 --- a/MediaBrowser.Model/Net/IpAddressInfo.cs +++ b/MediaBrowser.Model/Net/IpAddressInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6); public string Address { get; set; } + public IpAddressInfo SubnetMask { get; set; } public IpAddressFamily AddressFamily { get; set; } public IpAddressInfo(string address, IpAddressFamily addressFamily) diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index ef75f997fb..c99d684a13 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure /// /// Sends a message to the SSDP multicast address and port. /// - Task SendMulticastMessage(string message, CancellationToken cancellationToken); - Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken); #endregion @@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure #endregion } -} \ No newline at end of file +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 9da906b496..ea4d79c991 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -240,15 +240,15 @@ namespace Rssdp.Infrastructure } } - public Task SendMulticastMessage(string message, CancellationToken cancellationToken) + public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { - return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken); + return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken); } /// /// Sends a message to the SSDP multicast address and port. /// - public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken) + public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { if (message == null) throw new ArgumentNullException(nameof(message)); @@ -268,7 +268,7 @@ namespace Rssdp.Infrastructure IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork), Port = SsdpConstants.MulticastPort - }, cancellationToken).ConfigureAwait(false); + }, fromLocalIpAddress, cancellationToken).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false); } @@ -336,14 +336,15 @@ namespace Rssdp.Infrastructure #region Private Methods - private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken) + private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken) { var sockets = _sendSockets; if (sockets != null) { sockets = sockets.ToList(); - var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); + var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress))) + .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 128bdfcbb4..e17e14c1a6 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure var message = BuildMessage(header, values); - return _CommunicationsServer.SendMulticastMessage(message, cancellationToken); + return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); } private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index ce64ba1176..076246b243 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; +using MediaBrowser.Common.Net; using Rssdp; namespace Rssdp.Infrastructure @@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure /// public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher { + private readonly INetworkManager _networkManager; private ISsdpCommunicationsServer _CommsServer; private string _OSName; private string _OSVersion; + private bool _sendOnlyMatchedHost; private bool _SupportPnpRootDevice; @@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure /// /// Default constructor. /// - public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion) + public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager, + string osName, string osVersion, bool sendOnlyMatchedHost) { if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer)); + if (networkManager == null) throw new ArgumentNullException(nameof(networkManager)); if (osName == null) throw new ArgumentNullException(nameof(osName)); if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName)); if (osVersion == null) throw new ArgumentNullException(nameof(osVersion)); @@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure _RecentSearchRequests = new Dictionary(StringComparer.OrdinalIgnoreCase); _Random = new Random(); + _networkManager = networkManager; _CommsServer = communicationsServer; _CommsServer.RequestReceived += CommsServer_RequestReceived; _OSName = osName; _OSVersion = osVersion; + _sendOnlyMatchedHost = sendOnlyMatchedHost; _CommsServer.BeginListeningForBroadcasts(); } @@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + if (!_sendOnlyMatchedHost || + _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask)) + { + SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + } } } else @@ -427,7 +438,12 @@ namespace Rssdp.Infrastructure var message = BuildMessage(header, values); - _CommsServer.SendMulticastMessage(message, cancellationToken); + if (_sendOnlyMatchedHost) + { + _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); + } else { + _CommsServer.SendMulticastMessage(message, null, cancellationToken); + } //WriteTrace(String.Format("Sent alive notification"), device); } @@ -472,7 +488,13 @@ namespace Rssdp.Infrastructure var sendCount = IsDisposed ? 1 : 3; WriteTrace(String.Format("Sent byebye notification"), device); - return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken); + if (_sendOnlyMatchedHost) + { + return _CommsServer.SendMulticastMessage(message, sendCount, + _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); + } else { + return _CommsServer.SendMulticastMessage(message, sendCount, null, cancellationToken); + } } private void DisposeRebroadcastTimer() diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs index a2b0f60f54..d918b9040d 100644 --- a/RSSDP/SsdpRootDevice.cs +++ b/RSSDP/SsdpRootDevice.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using System.Xml; using Rssdp.Infrastructure; +using MediaBrowser.Model.Net; namespace Rssdp { @@ -52,6 +53,15 @@ namespace Rssdp /// public Uri Location { get; set; } + /// + /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required. + /// + public IpAddressInfo Address { get; set; } + + /// + /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required. + /// + public IpAddressInfo SubnetMask { get; set; } /// /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional. From 1eb26bdf080b178482e3fab81adb263c697e0a42 Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Fri, 22 Feb 2019 20:14:04 +0800 Subject: [PATCH 0049/6208] Ignore IPv6 DLNA devices DLNA is not ready for IPv6 now, uncomment the code will be fine. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 427b3a5c0e..d066bd1bd9 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -253,10 +253,10 @@ namespace Emby.Dlna.Main foreach (var address in addresses) { // TODO: Remove this condition on platforms that support it - //if (address.AddressFamily == IpAddressFamily.InterNetworkV6) - //{ - // continue; - //} + if (address.AddressFamily == IpAddressFamily.InterNetworkV6) + { + continue; + } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; From 0c49079c16f62c85fe529150a9777e9f2ccc440f Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Sat, 23 Feb 2019 09:56:55 +0800 Subject: [PATCH 0050/6208] Update comments for DLNA IPv6 --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- RSSDP/SsdpCommunicationsServer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index d066bd1bd9..5a7c9b617e 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -252,9 +252,9 @@ namespace Emby.Dlna.Main foreach (var address in addresses) { - // TODO: Remove this condition on platforms that support it if (address.AddressFamily == IpAddressFamily.InterNetworkV6) { + // Not support IPv6 right now continue; } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index ea4d79c991..d9a4b6ac01 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -372,7 +372,7 @@ namespace Rssdp.Infrastructure { if (address.AddressFamily == IpAddressFamily.InterNetworkV6) { - // Not supported ? + // Not support IPv6 right now continue; } From 47966793c0b9ca1d79f35f6010c0a256a4ab12b0 Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Sat, 23 Feb 2019 10:15:38 +0800 Subject: [PATCH 0051/6208] Remove useless if..else in SsdpDevicePublisher --- RSSDP/SsdpDevicePublisher.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 076246b243..921f33c212 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -438,12 +438,7 @@ namespace Rssdp.Infrastructure var message = BuildMessage(header, values); - if (_sendOnlyMatchedHost) - { - _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); - } else { - _CommsServer.SendMulticastMessage(message, null, cancellationToken); - } + _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); //WriteTrace(String.Format("Sent alive notification"), device); } @@ -488,13 +483,7 @@ namespace Rssdp.Infrastructure var sendCount = IsDisposed ? 1 : 3; WriteTrace(String.Format("Sent byebye notification"), device); - if (_sendOnlyMatchedHost) - { - return _CommsServer.SendMulticastMessage(message, sendCount, - _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); - } else { - return _CommsServer.SendMulticastMessage(message, sendCount, null, cancellationToken); - } + return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } private void DisposeRebroadcastTimer() From eb95b025d43b1d937fccaa0b0f10cbce24fcfae1 Mon Sep 17 00:00:00 2001 From: Lynxy Date: Fri, 22 Feb 2019 23:28:19 -0500 Subject: [PATCH 0052/6208] Add to contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 758202af6c..0831e1340e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,6 +21,7 @@ - [WillWill56](https://github.com/WillWill56) - [Liggy](https://github.com/Liggy) - [fruhnow](https://github.com/fruhnow) + - [Lynxy](https://github.com/Lynxy) # Emby Contributors From 67f399dccf8f6887397d8c308ea5c0c93d2e4e06 Mon Sep 17 00:00:00 2001 From: The Lynxy Date: Sat, 23 Feb 2019 10:01:41 -0500 Subject: [PATCH 0053/6208] Leave +genpts untouched in GetProgressiveVideoArguments() --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 264a45d24a..e378c2b89d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2436,8 +2436,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - args += " -flags -global_header -fflags +genpts"; - if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) @@ -2449,6 +2447,11 @@ namespace MediaBrowser.Controller.MediaEncoding { args += " -copyts -avoid_negative_ts disabled -start_at_zero"; } + + if (!state.RunTimeTicks.HasValue) + { + args += " -flags -global_header -fflags +genpts"; + } } else { From c2e57aba27e883e0418c9c50fc9323380e67aa33 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 23 Feb 2019 20:07:05 -0500 Subject: [PATCH 0054/6208] Add Debian armhf (Rasberry Pi) build plus crossbuild --- .../debian-package-armhf/Dockerfile.amd64 | 42 +++++++++++++++++++ .../debian-package-armhf/Dockerfile.armhf | 34 +++++++++++++++ deployment/debian-package-armhf/clean.sh | 29 +++++++++++++ .../debian-package-armhf/dependencies.txt | 1 + .../debian-package-armhf/docker-build.sh | 20 +++++++++ deployment/debian-package-armhf/package.sh | 42 +++++++++++++++++++ deployment/debian-package-armhf/pkg-src | 1 + deployment/debian-package-x64/pkg-src/rules | 18 +++++++- 8 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 deployment/debian-package-armhf/Dockerfile.amd64 create mode 100644 deployment/debian-package-armhf/Dockerfile.armhf create mode 100755 deployment/debian-package-armhf/clean.sh create mode 100644 deployment/debian-package-armhf/dependencies.txt create mode 100755 deployment/debian-package-armhf/docker-build.sh create mode 100755 deployment/debian-package-armhf/package.sh create mode 120000 deployment/debian-package-armhf/pkg-src diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/debian-package-armhf/Dockerfile.amd64 new file mode 100644 index 0000000000..0d62352e0b --- /dev/null +++ b/deployment/debian-package-armhf/Dockerfile.amd64 @@ -0,0 +1,42 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Prepare the cross-toolchain +RUN dpkg --add-architecture armhf \ + && apt-get update \ + && apt-get install -y cross-gcc-dev \ + && TARGET_LIST="armhf" cross-gcc-gensource 6 \ + && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ + && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf new file mode 100644 index 0000000000..eb4152116f --- /dev/null +++ b/deployment/debian-package-armhf/Dockerfile.armhf @@ -0,0 +1,34 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=armhf + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh new file mode 100755 index 0000000000..3898110aff --- /dev/null +++ b/deployment/debian-package-armhf/clean.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +keep_artifacts="${1}" + +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_armhf-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/debian-package-armhf/dependencies.txt b/deployment/debian-package-armhf/dependencies.txt new file mode 100644 index 0000000000..bdb9670965 --- /dev/null +++ b/deployment/debian-package-armhf/dependencies.txt @@ -0,0 +1 @@ +docker diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh new file mode 100755 index 0000000000..45e68f0c6b --- /dev/null +++ b/deployment/debian-package-armhf/docker-build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Builds the DEB inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image +sed -i '/dotnet-sdk-2.2,/d' debian/control + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -aarmhf + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/deb +mv /jellyfin_* ${ARTIFACT_DIR}/deb/ diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh new file mode 100755 index 0000000000..0ec0dc95cf --- /dev/null +++ b/deployment/debian-package-armhf/package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +ARCH="$( arch )" +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_armhf-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Determine which Dockerfile to use +case $ARCH in + 'x86_64') + DOCKERFILE="Dockerfile.amd64" + ;; + 'armv7l') + DOCKERFILE="Dockerfile.armhf" + ;; +esac + +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +# Correct ownership on the DEBs (as current user, then as root if that fails) +chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ + || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-armhf/pkg-src b/deployment/debian-package-armhf/pkg-src new file mode 120000 index 0000000000..4c695fea17 --- /dev/null +++ b/deployment/debian-package-armhf/pkg-src @@ -0,0 +1 @@ +../debian-package-x64/pkg-src \ No newline at end of file diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules index ce98cb8f86..15cbefa4e0 100644 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/deployment/debian-package-x64/pkg-src/rules @@ -2,7 +2,23 @@ CONFIG := Release TERM := xterm SHELL := /bin/bash -DOTNETRUNTIME := debian-x64 + +HOST_ARCH := $(shell arch) +BUILD_ARCH := ${DEB_HOST_MULTIARCH} +ifeq ($(HOST_ARCH),x86_64) + ifeq ($(BUILD_ARCH),arm-linux-gnueabihf) + # Cross-building ARM on AMD64 + DOTNETRUNTIME := debian-arm + else + # Building AMD64 + DOTNETRUNTIME := debian-x64 + endif +endif +ifeq ($(HOST_ARCH),armv7l) + # Building ARM + DOTNETRUNTIME := debian-arm +endif + export DH_VERBOSE=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1 From da61998ad611bf25d5644223a82eed1cf2e3d835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20B=C3=BCttner?= Date: Fri, 22 Feb 2019 13:52:32 +0100 Subject: [PATCH 0055/6208] Build releases without debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Büttner --- Dockerfile | 1 + Dockerfile.arm | 1 + Dockerfile.arm64 | 1 + deployment/common.build.sh | 4 ++-- deployment/debian-package-x64/pkg-src/rules | 3 ++- deployment/fedora-package-x64/pkg-src/jellyfin.spec | 4 ++-- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 978b0d5409..af570ba655 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 RUN dotnet publish \ --configuration release \ --output /jellyfin \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ Jellyfin.Server FROM jellyfin/ffmpeg as ffmpeg diff --git a/Dockerfile.arm b/Dockerfile.arm index 9d1c30619b..9316245c50 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -21,6 +21,7 @@ RUN dotnet publish \ -r linux-arm \ --configuration release \ --output /jellyfin \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ Jellyfin.Server diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index e61aaa167c..5c67f85c9b 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -22,6 +22,7 @@ RUN dotnet publish \ -r linux-arm64 \ --configuration release \ --output /jellyfin \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ Jellyfin.Server diff --git a/deployment/common.build.sh b/deployment/common.build.sh index c191ec2a1b..5bffa1a732 100755 --- a/deployment/common.build.sh +++ b/deployment/common.build.sh @@ -36,9 +36,9 @@ build_jellyfin() echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}" if [[ $DOTNETRUNTIME == 'framework' ]]; then - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" + dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" else - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} + dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" fi EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules index ce98cb8f86..646f3621fe 100644 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/deployment/debian-package-x64/pkg-src/rules @@ -16,7 +16,8 @@ override_dh_auto_test: override_dh_clistrip: override_dh_auto_build: - dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server + dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 451a304b76..35045ee669 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an %install export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server +dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE %{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json @@ -73,7 +74,6 @@ EOF %{_libdir}/%{name}/jellyfin-web/* %attr(755,root,root) %{_bindir}/%{name} %{_libdir}/%{name}/*.json -%{_libdir}/%{name}/*.pdb %{_libdir}/%{name}/*.dll %{_libdir}/%{name}/*.so %{_libdir}/%{name}/*.a From 38ec68c488c0c8e88989dc7c3fdc0824cfe71343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20B=C3=BCttner?= Date: Sun, 24 Feb 2019 11:17:39 +0100 Subject: [PATCH 0056/6208] use common.build.sh for docker image builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Büttner --- Dockerfile | 7 ++----- Dockerfile.arm | 8 ++------ Dockerfile.arm64 | 8 ++------ deployment/common.build.sh | 3 +-- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index af570ba655..3562688419 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -RUN dotnet publish \ - --configuration release \ - --output /jellyfin \ - "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin" FROM jellyfin/ffmpeg as ffmpeg FROM microsoft/dotnet:${DOTNET_VERSION}-runtime diff --git a/Dockerfile.arm b/Dockerfile.arm index 9316245c50..69c5d446e8 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -17,12 +17,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish \ - -r linux-arm \ - --configuration release \ - --output /jellyfin \ - "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 5c67f85c9b..b85f8735bd 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -18,12 +18,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish \ - -r linux-arm64 \ - --configuration release \ - --output /jellyfin \ - "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" \ - Jellyfin.Server +RUN bash -c "source deployment/common.build.sh && \ + build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 diff --git a/deployment/common.build.sh b/deployment/common.build.sh index 5bffa1a732..d028e3a668 100755 --- a/deployment/common.build.sh +++ b/deployment/common.build.sh @@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release" DEFAULT_OUTPUT_DIR="dist/jellyfin-git" DEFAULT_PKG_DIR="pkg-dist" DEFAULT_DOCKERFILE="Dockerfile" -DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD` DEFAULT_ARCHIVE_CMD="tar -xvzf" # Parse the version from the AssemblyVersion @@ -53,7 +52,7 @@ build_jellyfin_docker() ( BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT} DOCKERFILE=${2-$DEFAULT_DOCKERFILE} - IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG} + IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"} echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}" docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT} From 2e9a3d45c2a03f930856d038867a7caff01f208f Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 16:16:53 +0100 Subject: [PATCH 0057/6208] Fix slow local image validation (#990) * Check for local image directory existence to avoid tons of exceptions --- MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs | 7 +++++++ MediaBrowser.Providers/Manager/MetadataService.cs | 5 +---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 1a7654bfda..7c330ad862 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images var path = item.ContainingFolderPath; + // Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs... + if (!Directory.Exists(path)) + { + return Array.Empty(); + } + if (includeDirectories) { return directoryService.GetFileSystemEntries(path) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 77028e526a..f0716f2017 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { localImagesFailed = true; - if (!(item is IItemByName)) - { - Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name"); - } + Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name"); } var metadataResult = new MetadataResult From aafed63c3f635ccac82be41b5a8f272121397b79 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 16:33:05 +0100 Subject: [PATCH 0058/6208] Set EnableRaisingEvents to true for processes that require it --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index d922f1068a..7f29c06b4c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -694,7 +694,8 @@ namespace MediaBrowser.MediaEncoding.Encoder FileName = FFMpegPath, Arguments = args, IsHidden = true, - ErrorDialog = false + ErrorDialog = false, + EnableRaisingEvents = true }); _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -816,7 +817,8 @@ namespace MediaBrowser.MediaEncoding.Encoder FileName = FFMpegPath, Arguments = args, IsHidden = true, - ErrorDialog = false + ErrorDialog = false, + EnableRaisingEvents = true }); _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); From 96b3d37caf269789015900bd7f6fa24f17652848 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 21:24:57 +0100 Subject: [PATCH 0059/6208] Check that ffmpeg log target isn't disposed before writing to it --- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index b812a8ddc6..5cd6fd27a6 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -24,7 +24,8 @@ namespace MediaBrowser.Controller.MediaEncoding { using (var reader = new StreamReader(source)) { - while (!reader.EndOfStream) + // If ffmpeg process is closed, the state is disposed, so don't write to target in that case + while (!reader.EndOfStream && target.CanWrite) { var line = await reader.ReadLineAsync().ConfigureAwait(false); @@ -37,11 +38,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } catch (Exception ex) { _logger.LogError(ex, "Error reading ffmpeg log"); From 6e07eab247f51532e74de7ceaaf567156c46e39c Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 21:44:56 +0100 Subject: [PATCH 0060/6208] Don't close the socket response multiple times --- .../SocketSharp/WebSocketSharpListener.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 736f9feef8..3b98753181 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -129,6 +129,7 @@ namespace Jellyfin.Server.SocketSharp private async Task ProcessWebSocketRequest(HttpListenerContext ctx) { + int statusCode = 200; try { var endpoint = ctx.Request.RemoteEndPoint.ToString(); @@ -164,33 +165,23 @@ namespace Jellyfin.Server.SocketSharp Endpoint = endpoint }); - await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); + await socket.StartReceive().ConfigureAwait(false); } } else { _logger.LogWarning("Web socket connection not allowed"); - ctx.Response.StatusCode = 401; - ctx.Response.Close(); + statusCode = 401; } } catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); - ctx.Response.StatusCode = 500; - ctx.Response.Close(); - } - } - - private async Task ReceiveWebSocketAsync(HttpListenerContext ctx, SharpWebSocket socket) - { - try - { - await socket.StartReceive().ConfigureAwait(false); + statusCode = 500; } finally { - TryClose(ctx, 200); + TryClose(ctx, statusCode); } } @@ -201,10 +192,6 @@ namespace Jellyfin.Server.SocketSharp ctx.Response.StatusCode = statusCode; ctx.Response.Close(); } - catch (ObjectDisposedException) - { - // TODO: Investigate and properly fix. - } catch (Exception ex) { _logger.LogError(ex, "Error closing web socket response"); From 547d0ecf584556019aa743687009a0f0d3a8f9dd Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 22:04:30 +0100 Subject: [PATCH 0061/6208] Move the check further down --- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 5cd6fd27a6..46593fb2fc 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -24,8 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding { using (var reader = new StreamReader(source)) { - // If ffmpeg process is closed, the state is disposed, so don't write to target in that case - while (!reader.EndOfStream && target.CanWrite) + while (!reader.EndOfStream) { var line = await reader.ReadLineAsync().ConfigureAwait(false); @@ -33,6 +32,12 @@ namespace MediaBrowser.Controller.MediaEncoding var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + // If ffmpeg process is closed, the state is disposed, so don't write to target in that case + if (!target.CanWrite) + { + break; + } + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); await target.FlushAsync().ConfigureAwait(false); } From 5262e50fee325bff1e0996a7cc6d43fd1c17f7be Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 22:28:46 +0100 Subject: [PATCH 0062/6208] Try another fix --- Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 3b98753181..9c8d3e1d81 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -171,17 +171,13 @@ namespace Jellyfin.Server.SocketSharp else { _logger.LogWarning("Web socket connection not allowed"); - statusCode = 401; + TryClose(ctx, 401); } } catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); - statusCode = 500; - } - finally - { - TryClose(ctx, statusCode); + TryClose(ctx, 500); } } From 4df3333b7116636e56967b332c0e66b6c36960c2 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Feb 2019 22:31:46 +0100 Subject: [PATCH 0063/6208] Remove the unused status code --- Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 9c8d3e1d81..693c2328c6 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -129,7 +129,6 @@ namespace Jellyfin.Server.SocketSharp private async Task ProcessWebSocketRequest(HttpListenerContext ctx) { - int statusCode = 200; try { var endpoint = ctx.Request.RemoteEndPoint.ToString(); From dac2c98d8aebe9e18c7b6631921bba7676f74daf Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 24 Feb 2019 23:20:04 -0500 Subject: [PATCH 0064/6208] Disable documentation and debug in build --- deployment/debian-package-x64/pkg-src/rules | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules index 15cbefa4e0..62f75bc6b1 100644 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/deployment/debian-package-x64/pkg-src/rules @@ -32,7 +32,8 @@ override_dh_auto_test: override_dh_clistrip: override_dh_auto_build: - dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server + dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ + "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true From 5054a77dcf73f829178009c73fef7b96de644607 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Mon, 25 Feb 2019 00:41:34 -0500 Subject: [PATCH 0065/6208] Fix the ffmpeg compatibility Doing this the other way was just complex. No longer try to override the system ffmpeg, just put ours somewhere else and depend on that package. --- deployment/debian-package-x64/pkg-src/conf/jellyfin | 6 +++--- deployment/debian-package-x64/pkg-src/control | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index b052b2ec66..58fe79332a 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" # Restart script for in-app server control JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" -# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values -#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg" -#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe" +# ffmpeg binary paths, overriding the system values +JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg" +JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control index 88d10438b4..d96660590c 100644 --- a/deployment/debian-package-x64/pkg-src/control +++ b/deployment/debian-package-x64/pkg-src/control @@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Architecture: any Depends: at, libsqlite3-0, - ffmpeg (<7:4.1) | jellyfin-ffmpeg, + jellyfin-ffmpeg, libfontconfig1, libfreetype6, libssl1.0.0 | libssl1.0.2 From 0f9006c81fae1c9bd74c284b0affa76115a89ad2 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 25 Feb 2019 18:26:17 +0100 Subject: [PATCH 0066/6208] Use stopwatch for more accurate measurements and reduce log spam DateTime.Now is suitible for small timespans Replaced the needlessly complex and verbose logging for the httpserver --- .../ApplicationHost.cs | 10 ++- .../HttpServer/HttpListenerHost.cs | 82 +++++-------------- .../HttpServer/LoggerUtils.cs | 55 ------------- .../ScheduledTasks/Triggers/DailyTrigger.cs | 2 +- 4 files changed, 27 insertions(+), 122 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpServer/LoggerUtils.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 042b04b3bb..553d9b9dd8 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -550,16 +550,18 @@ namespace Emby.Server.Implementations var entryPoints = GetExports(); - var now = DateTime.UtcNow; + var stopWatch = new Stopwatch(); + stopWatch.Start(); await Task.WhenAll(StartEntryPoints(entryPoints, true)); - Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:fff} ms", DateTime.Now - now); + Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:fff} ms", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); HttpServer.GlobalResponse = null; - now = DateTime.UtcNow; + stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)); - Logger.LogInformation("Executed all post-startup entry points in {Elapsed:fff} ms", DateTime.Now - now); + Logger.LogInformation("Executed all post-startup entry points in {Elapsed:fff} ms", stopWatch.Elapsed); + stopWatch.Stop(); } private IEnumerable StartEntryPoints(IEnumerable entryPoints, bool isBeforeStartup) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index d78891ac7c..29aab353d8 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -286,31 +287,6 @@ namespace Emby.Server.Implementations.HttpServer } } - private static readonly string[] _skipLogExtensions = - { - ".js", - ".css", - ".woff", - ".woff2", - ".ttf", - ".html" - }; - - private bool EnableLogging(string url, string localPath) - { - var extension = GetExtension(url); - - return ((string.IsNullOrEmpty(extension) || !_skipLogExtensions.Contains(extension)) - && (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)); - } - - private static string GetExtension(string url) - { - var parts = url.Split(new[] { '?' }, 2); - - return Path.GetExtension(parts[0]); - } - public static string RemoveQueryStringByKey(string url, string key) { var uri = new Uri(url); @@ -448,10 +424,9 @@ namespace Emby.Server.Implementations.HttpServer /// protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { - var date = DateTime.Now; + var stopWatch = new Stopwatch(); + stopWatch.Start(); var httpRes = httpReq.Response; - bool enableLog = false; - bool logHeaders = false; string urlToLog = null; string remoteIp = httpReq.RemoteIp; @@ -498,18 +473,8 @@ namespace Emby.Server.Implementations.HttpServer return; } - var operationName = httpReq.OperationName; - - enableLog = EnableLogging(urlString, localPath); - urlToLog = urlString; - logHeaders = enableLog && urlToLog.IndexOf("/videos/", StringComparison.OrdinalIgnoreCase) != -1; - - if (enableLog) - { - urlToLog = GetUrlToLog(urlString); - - LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent, logHeaders ? httpReq.Headers : null); - } + urlToLog = GetUrlToLog(urlString); + Logger.LogDebug("HTTP {HttpMethod} {Url} UserAgent: {UserAgent} \nHeaders: {@Headers}", urlToLog, httpReq.UserAgent ?? string.Empty, httpReq.HttpMethod, httpReq.Headers); if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) @@ -517,6 +482,7 @@ namespace Emby.Server.Implementations.HttpServer RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) { @@ -562,16 +528,19 @@ namespace Emby.Server.Implementations.HttpServer RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) { RedirectToUrl(httpRes, "../" + DefaultRedirectPath); return; } + if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { RedirectToUrl(httpRes, DefaultRedirectPath); return; } + if (string.IsNullOrEmpty(localPath)) { RedirectToUrl(httpRes, "/" + DefaultRedirectPath); @@ -607,33 +576,21 @@ namespace Emby.Server.Implementations.HttpServer if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, operationName, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false); } else { await ErrorHandler(new FileNotFoundException(), httpReq, false, false).ConfigureAwait(false); } } - catch (OperationCanceledException ex) + catch (Exception ex) when (ex is SocketException || ex is IOException || ex is OperationCanceledException) { await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); } - - catch (IOException ex) - { - await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); - } - - catch (SocketException ex) - { - await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); - } - catch (SecurityException ex) { await ErrorHandler(ex, httpReq, false, true).ConfigureAwait(false); } - catch (Exception ex) { var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase); @@ -644,13 +601,15 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.Close(); - if (enableLog) + stopWatch.Stop(); + var elapsed = stopWatch.Elapsed; + if (elapsed.Milliseconds > 500) { - var statusCode = httpRes.StatusCode; - - var duration = DateTime.Now - date; - - LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration, logHeaders ? httpRes.Headers : null); + _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:ss.fff}. {Url}", httpRes.StatusCode, remoteIp, stopWatch.Elapsed, urlToLog); + } + else + { + _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:ss.fff}. {Url}", httpRes.StatusCode, remoteIp, stopWatch.Elapsed, urlToLog); } } } @@ -663,12 +622,11 @@ namespace Emby.Server.Implementations.HttpServer var pathParts = pathInfo.TrimStart('/').Split('/'); if (pathParts.Length == 0) { - _logger.LogError("Path parts empty for PathInfo: {pathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); + _logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); return null; } var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType); - if (restPath != null) { return new ServiceHandler diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs deleted file mode 100644 index d22d9db265..0000000000 --- a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.HttpServer -{ - public static class LoggerUtils - { - public static void LogRequest(ILogger logger, string url, string method, string userAgent, QueryParamCollection headers) - { - if (headers == null) - { - logger.LogInformation("{0} {1}. UserAgent: {2}", "HTTP " + method, url, userAgent ?? string.Empty); - } - else - { - var headerText = string.Empty; - var index = 0; - - foreach (var i in headers) - { - if (index > 0) - { - headerText += ", "; - } - - headerText += i.Name + "=" + i.Value; - - index++; - } - - logger.LogInformation("HTTP {0} {1}. {2}", method, url, headerText); - } - } - - /// - /// Logs the response. - /// - /// The logger. - /// The status code. - /// The URL. - /// The end point. - /// The duration. - public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration, QueryParamCollection headers) - { - var durationMs = duration.TotalMilliseconds; - var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms"; - - //var headerText = headers == null ? string.Empty : "Headers: " + string.Join(", ", headers.Where(i => i.Name.IndexOf("Access-", StringComparison.OrdinalIgnoreCase) == -1).Select(i => i.Name + "=" + i.Value).ToArray()); - var headerText = string.Empty; - logger.LogInformation("HTTP Response {0} to {1}. Time: {2}{3}. {4} {5}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url, headerText); - } - } -} diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index 98685cebea..d33ce8a1ea 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var dueTime = triggerDate - now; - logger.LogInformation("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate.ToString(), dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + logger.LogInformation("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate, dueTime); Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } From 9ba6227db4435ca47ea569ad2b75a417e75fbad6 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 16 Feb 2019 16:54:18 +0100 Subject: [PATCH 0067/6208] Less string allocations --- Jellyfin.Server/SocketSharp/RequestMono.cs | 25 +++---- .../SocketSharp/WebSocketSharpRequest.cs | 66 +++++++++++-------- MediaBrowser.WebDashboard/jellyfin-web | 2 +- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Jellyfin.Server/SocketSharp/RequestMono.cs b/Jellyfin.Server/SocketSharp/RequestMono.cs index f2a08c9ae2..8396ad600d 100644 --- a/Jellyfin.Server/SocketSharp/RequestMono.cs +++ b/Jellyfin.Server/SocketSharp/RequestMono.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Server.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { - internal static string GetParameter(string header, string attr) + internal static string GetParameter(ReadOnlySpan header, string attr) { int ap = header.IndexOf(attr, StringComparison.Ordinal); if (ap == -1) @@ -31,13 +31,14 @@ namespace Jellyfin.Server.SocketSharp ending = ' '; } - int end = header.IndexOf(ending, ap + 1); + var slice = header.Slice(ap + 1); + int end = slice.IndexOf(ending); if (end == -1) { - return ending == '"' ? null : header.Substring(ap); + return ending == '"' ? null : header.Slice(ap).ToString(); } - return header.Substring(ap + 1, end - ap - 1); + return slice.Slice(0, end - ap - 1).ToString(); } private async Task LoadMultiPart(WebROCollection form) @@ -394,7 +395,7 @@ namespace Jellyfin.Server.SocketSharp } var elem = new Element(); - string header; + ReadOnlySpan header; while ((header = ReadHeaders()) != null) { if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) @@ -404,7 +405,7 @@ namespace Jellyfin.Server.SocketSharp } else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase)) { - elem.ContentType = header.Substring("Content-Type:".Length).Trim(); + elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString(); elem.Encoding = GetEncoding(elem.ContentType); } } @@ -452,7 +453,7 @@ namespace Jellyfin.Server.SocketSharp return sb.ToString(); } - private static string GetContentDispositionAttribute(string l, string name) + private static string GetContentDispositionAttribute(ReadOnlySpan l, string name) { int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); if (idx < 0) @@ -461,7 +462,7 @@ namespace Jellyfin.Server.SocketSharp } int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); + int end = l.Slice(begin).IndexOf('"'); if (end < 0) { return null; @@ -472,10 +473,10 @@ namespace Jellyfin.Server.SocketSharp return string.Empty; } - return l.Substring(begin, end - begin); + return l.Slice(begin, end - begin).ToString(); } - private string GetContentDispositionAttributeWithEncoding(string l, string name) + private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan l, string name) { int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); if (idx < 0) @@ -484,7 +485,7 @@ namespace Jellyfin.Server.SocketSharp } int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); + int end = l.Slice(begin).IndexOf('"'); if (end < 0) { return null; @@ -495,7 +496,7 @@ namespace Jellyfin.Server.SocketSharp return string.Empty; } - string temp = l.Substring(begin, end - begin); + ReadOnlySpan temp = l.Slice(begin, end - begin); byte[] source = new byte[temp.Length]; for (int i = temp.Length - 1; i >= 0; i--) { diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs index 6458707d92..069f47f9ab 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs @@ -57,18 +57,37 @@ namespace Jellyfin.Server.SocketSharp public string XRealIp => string.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; private string remoteIp; - public string RemoteIp => - remoteIp ?? - (remoteIp = CheckBadChars(XForwardedFor) ?? - NormalizeIp(CheckBadChars(XRealIp) ?? - (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); + public string RemoteIp + { + get + { + if (remoteIp != null) + { + return remoteIp; + } + + var temp = CheckBadChars(XForwardedFor); + if (temp.Length != 0) + { + return remoteIp = temp.ToString(); + } + + temp = CheckBadChars(XRealIp); + if (temp.Length != 0) + { + return remoteIp = NormalizeIp(temp).ToString(); + } + + return remoteIp = NormalizeIp(request.RemoteEndPoint?.Address.ToString()).ToString(); + } + } private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; // CheckBadChars - throws on invalid chars to be not found in header name/value - internal static string CheckBadChars(string name) + internal static ReadOnlySpan CheckBadChars(ReadOnlySpan name) { - if (name == null || name.Length == 0) + if (name.Length == 0) { return name; } @@ -99,7 +118,7 @@ namespace Jellyfin.Server.SocketSharp } else if (c == 127 || (c < ' ' && c != '\t')) { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); + throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name)); } break; @@ -113,7 +132,7 @@ namespace Jellyfin.Server.SocketSharp break; } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } case 2: @@ -124,14 +143,14 @@ namespace Jellyfin.Server.SocketSharp break; } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } } } if (crlf != 0) { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); } return name; @@ -150,16 +169,16 @@ namespace Jellyfin.Server.SocketSharp return false; } - private string NormalizeIp(string ip) + private ReadOnlySpan NormalizeIp(ReadOnlySpan ip) { - if (!string.IsNullOrWhiteSpace(ip)) + if (ip.Length != 0 && !ip.IsWhiteSpace()) { // Handle ipv4 mapped to ipv6 const string srch = "::ffff:"; var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); if (index == 0) { - ip = ip.Substring(srch.Length); + ip = ip.Slice(srch.Length); } } @@ -302,17 +321,6 @@ namespace Jellyfin.Server.SocketSharp return null; } - public static string LeftPart(string strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle, StringComparison.Ordinal); - return pos == -1 ? strVal : strVal.Substring(0, pos); - } - public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle) { if (strVal == null) @@ -350,7 +358,7 @@ namespace Jellyfin.Server.SocketSharp } this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode); + this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString(); } return this.pathInfo; @@ -517,14 +525,14 @@ namespace Jellyfin.Server.SocketSharp } } - public static string NormalizePathInfo(string pathInfo, string handlerPath) + public static ReadOnlySpan NormalizePathInfo(string pathInfo, string handlerPath) { if (handlerPath != null) { - var trimmed = pathInfo.TrimStart('/'); + var trimmed = pathInfo.AsSpan().TrimStart('/'); if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase)) { - return trimmed.Substring(handlerPath.Length); + return trimmed.Slice(handlerPath.Length).ToString(); } } diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index ec5a3b6e5e..b4842e325e 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d +Subproject commit b4842e325e9d7d708193b4a27060cfe4c978df5e From a0606b573042f1ec0846f26e51f697f5bde70b11 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Feb 2019 17:11:27 +0100 Subject: [PATCH 0068/6208] Don't change submodule --- MediaBrowser.WebDashboard/jellyfin-web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index b4842e325e..ec5a3b6e5e 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit b4842e325e9d7d708193b4a27060cfe4c978df5e +Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d From 0804bed66d6b060ab550ab4ef878724a14764fed Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Feb 2019 19:37:39 +0100 Subject: [PATCH 0069/6208] Log time in a standardized way --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 8 ++++---- .../ScheduledTasks/Triggers/DailyTrigger.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 553d9b9dd8..45a819f260 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -553,14 +553,14 @@ namespace Emby.Server.Implementations var stopWatch = new Stopwatch(); stopWatch.Start(); await Task.WhenAll(StartEntryPoints(entryPoints, true)); - Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:fff} ms", stopWatch.Elapsed); + Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); HttpServer.GlobalResponse = null; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)); - Logger.LogInformation("Executed all post-startup entry points in {Elapsed:fff} ms", stopWatch.Elapsed); + Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); stopWatch.Stop(); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 6502e4aeda..70e5fa6406 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2747,7 +2747,7 @@ namespace Emby.Server.Implementations.Data if (elapsed >= slowThreshold) { - Logger.LogWarning("{0} query time (slow): {1}ms. Query: {2}", + Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}", methodName, elapsed, commandText); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 29aab353d8..ee746c6696 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -68,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - + _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); Instance = this; @@ -603,13 +603,13 @@ namespace Emby.Server.Implementations.HttpServer stopWatch.Stop(); var elapsed = stopWatch.Elapsed; - if (elapsed.Milliseconds > 500) + if (elapsed.TotalMilliseconds > 500) { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:ss.fff}. {Url}", httpRes.StatusCode, remoteIp, stopWatch.Elapsed, urlToLog); + _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } else { - _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:ss.fff}. {Url}", httpRes.StatusCode, remoteIp, stopWatch.Elapsed, urlToLog); + _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index d33ce8a1ea..ec9466c4ab 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var dueTime = triggerDate - now; - logger.LogInformation("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate, dueTime); + logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:g}, which is {DueTime:g} from now.", taskName, triggerDate, dueTime); Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } From 1731bf7372a13ea8c656eb9f895508b7b4c66784 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Feb 2019 20:47:23 +0100 Subject: [PATCH 0070/6208] Remove ordering items --- Emby.Server.Implementations/Dto/DtoService.cs | 15 ++------ .../Session/SessionManager.cs | 2 +- MediaBrowser.Api/BaseApiService.cs | 13 ++----- MediaBrowser.Api/FilterService.cs | 3 +- MediaBrowser.Api/UserLibrary/ItemsService.cs | 35 +++++++------------ MediaBrowser.Controller/Dto/DtoOptions.cs | 14 ++------ MediaBrowser.Controller/Dto/IDtoService.cs | 4 +-- MediaBrowser.Controller/Entities/Folder.cs | 24 ++----------- 8 files changed, 27 insertions(+), 83 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2233d3d40c..7b28a22a84 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -5,8 +5,6 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; @@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto return GetBaseItemDto(item, options, user, owner); } - public BaseItemDto[] GetBaseItemDtos(List items, DtoOptions options, User user = null, BaseItem owner = null) - { - return GetBaseItemDtos(items, items.Count, options, user, owner); - } - - public BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null) - { - return GetBaseItemDtos(items, items.Length, options, user, owner); - } + public BaseItemDto[] GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null) + => GetBaseItemDtos(items, items.Count, options, user, owner); public BaseItemDto[] GetBaseItemDtos(IEnumerable items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index fa0ab62d32..03e7b26545 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private IList TranslateItemForPlayback(Guid id, User user) + private IEnumerable TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index a037357ede..69673a49c1 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -172,16 +172,9 @@ namespace MediaBrowser.Api if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes)) { - if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes)) - { - options.ImageTypes = Array.Empty(); - } - else - { - options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) - .ToArray(); - } + options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) + .ToArray(); } } diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 9caf07cea2..201efe7371 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -180,7 +181,7 @@ namespace MediaBrowser.Api return ToOptimizedResult(filters); } - private QueryFiltersLegacy GetFilters(BaseItem[] items) + private QueryFiltersLegacy GetFilters(IReadOnlyCollection items) { var result = new QueryFiltersLegacy(); diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 84475467fe..3c7ad1d0a6 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using MediaBrowser.Controller.Dto; @@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary request.ParentId = null; } - var item = string.IsNullOrEmpty(request.ParentId) ? - null : - _libraryManager.GetItemById(request.ParentId); + BaseItem item = null; + + if (!string.IsNullOrEmpty(request.ParentId)) + { + item = _libraryManager.GetItemById(request.ParentId); + } if (item == null) { - item = string.IsNullOrEmpty(request.ParentId) ? - user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() : - _libraryManager.GetItemById(request.ParentId); + item = _libraryManager.GetUserRootFolder(); } - // Default list type = children - - var folder = item as Folder; + Folder folder = item as Folder; if (folder == null) { - folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); + folder = _libraryManager.GetUserRootFolder(); } var hasCollectionType = folder as IHasCollectionType; - var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)); - - if (isPlaylistQuery) + if (hasCollectionType != null + && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { request.Recursive = true; request.IncludeItemTypes = "Playlist"; @@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary }; } - if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null) - { - return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); - } - - var userRoot = item as UserRootFolder; - - if (userRoot == null) + if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder)) { return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); } var itemsArray = folder.GetChildren(user, true).ToArray(); - return new QueryResult { Items = itemsArray, diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index aa99f6b58f..cdaf95f5ce 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto .ToArray(); public bool ContainsField(ItemFields field) - { - return AllItemFields.Contains(field); - } + => Fields.Contains(field); public DtoOptions(bool allFields) { @@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto EnableUserData = true; AddCurrentProgram = true; - if (allFields) - { - Fields = AllItemFields; - } - else - { - Fields = new ItemFields[] { }; - } - + Fields = allFields ? AllItemFields : Array.Empty(); ImageTypes = AllImageTypes; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index df5ec5dd0b..4b6fd58fea 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto /// The options. /// The user. /// The owner. - BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null); - - BaseItemDto[] GetBaseItemDtos(List items, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto[] GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null); /// /// Gets the item by name dto. diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8bfadbee6c..e49ff20baa 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities { if (query.ItemIds.Length > 0) { - var result = LibraryManager.GetItemsResult(query); - - if (query.OrderBy.Length == 0) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); - } - return result; + return LibraryManager.GetItemsResult(query); } return GetItemsInternal(query); } - public BaseItem[] GetItemList(InternalItemsQuery query) + public IReadOnlyList GetItemList(InternalItemsQuery query) { query.EnableTotalRecordCount = false; if (query.ItemIds.Length > 0) { - var result = LibraryManager.GetItemList(query); - - if (query.OrderBy.Length == 0) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); - } - return result.ToArray(); + return LibraryManager.GetItemList(query); } return GetItemsInternal(query).Items; From 4db31acff940dc9cabbd39649103faf4dc2e2c47 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 21 Feb 2019 10:07:21 +0100 Subject: [PATCH 0071/6208] Begin removing System.Net sources --- .../HttpClientManager/HttpClientManager.cs | 2 +- .../HttpServer/FileWriter.cs | 16 +- .../HttpServer/HttpListenerHost.cs | 3 + .../HttpServer/HttpResultFactory.cs | 2 +- .../HttpServer/RangeRequestWriter.cs | 2 +- .../HttpServer/ResponseFilter.cs | 2 +- .../HttpServer/StreamWriter.cs | 4 +- .../SocketSharp/WebSocketSharpListener.cs | 56 +- .../SocketSharp/WebSocketSharpRequest.cs | 3 +- .../SocketSharp/WebSocketSharpResponse.cs | 46 +- .../BaseProgressiveStreamingService.cs | 18 +- MediaBrowser.Model/Services/IRequest.cs | 2 +- .../Services/QueryParamCollection.cs | 31 + SocketHttpListener/Ext.cs | 1 - SocketHttpListener/HttpResponse.cs | 243 ++++---- .../Net/AuthenticationSchemeSelector.cs | 6 - SocketHttpListener/Net/AuthenticationTypes.cs | 9 - SocketHttpListener/Net/BoundaryType.cs | 11 - SocketHttpListener/Net/ChunkStream.cs | 385 ------------- SocketHttpListener/Net/ChunkedInputStream.cs | 178 ------ SocketHttpListener/Net/EntitySendFormat.cs | 8 - SocketHttpListener/Net/HttpConnection.cs | 528 ----------------- .../Net/HttpEndPointListener.cs | 526 ----------------- SocketHttpListener/Net/HttpEndPointManager.cs | 192 ------- .../Net/HttpKnownHeaderNames.cs | 91 --- SocketHttpListener/Net/HttpListener.cs | 284 --------- .../Net/HttpListenerBasicIdentity.cs | 49 -- .../Net/HttpListenerContext.Managed.cs | 99 ---- SocketHttpListener/Net/HttpListenerContext.cs | 74 --- .../Net/HttpListenerPrefixCollection.cs | 117 ---- .../Net/HttpListenerRequest.Managed.cs | 325 ----------- SocketHttpListener/Net/HttpListenerRequest.cs | 537 ------------------ .../Net/HttpListenerRequestUriBuilder.cs | 443 --------------- .../Net/HttpListenerResponse.Managed.cs | 333 ----------- .../Net/HttpListenerResponse.cs | 294 ---------- .../Net/HttpRequestStream.Managed.cs | 210 ------- SocketHttpListener/Net/HttpRequestStream.cs | 129 ----- .../Net/HttpResponseStream.Managed.cs | 329 ----------- SocketHttpListener/Net/HttpResponseStream.cs | 124 ---- SocketHttpListener/Net/HttpStatusCode.cs | 321 ----------- .../Net/HttpStatusDescription.cs | 69 --- .../Net/HttpStreamAsyncResult.cs | 79 --- SocketHttpListener/Net/HttpVersion.cs | 16 - SocketHttpListener/Net/ListenerPrefix.cs | 89 --- SocketHttpListener/Net/UriScheme.cs | 20 - SocketHttpListener/Net/WebHeaderCollection.cs | 360 ------------ SocketHttpListener/Net/WebHeaderEncoding.cs | 84 --- .../HttpListenerWebSocketContext.cs | 92 --- .../Net/WebSockets/HttpWebSocket.Managed.cs | 81 --- .../Net/WebSockets/HttpWebSocket.cs | 159 ------ .../Net/WebSockets/WebSocketCloseStatus.cs | 27 - .../Net/WebSockets/WebSocketContext.cs | 24 - .../Net/WebSockets/WebSocketValidate.cs | 141 ----- SocketHttpListener/WebSocket.cs | 3 +- 54 files changed, 264 insertions(+), 7013 deletions(-) delete mode 100644 SocketHttpListener/Net/AuthenticationSchemeSelector.cs delete mode 100644 SocketHttpListener/Net/AuthenticationTypes.cs delete mode 100644 SocketHttpListener/Net/BoundaryType.cs delete mode 100644 SocketHttpListener/Net/ChunkStream.cs delete mode 100644 SocketHttpListener/Net/ChunkedInputStream.cs delete mode 100644 SocketHttpListener/Net/EntitySendFormat.cs delete mode 100644 SocketHttpListener/Net/HttpConnection.cs delete mode 100644 SocketHttpListener/Net/HttpEndPointListener.cs delete mode 100644 SocketHttpListener/Net/HttpEndPointManager.cs delete mode 100644 SocketHttpListener/Net/HttpKnownHeaderNames.cs delete mode 100644 SocketHttpListener/Net/HttpListener.cs delete mode 100644 SocketHttpListener/Net/HttpListenerBasicIdentity.cs delete mode 100644 SocketHttpListener/Net/HttpListenerContext.Managed.cs delete mode 100644 SocketHttpListener/Net/HttpListenerContext.cs delete mode 100644 SocketHttpListener/Net/HttpListenerPrefixCollection.cs delete mode 100644 SocketHttpListener/Net/HttpListenerRequest.Managed.cs delete mode 100644 SocketHttpListener/Net/HttpListenerRequest.cs delete mode 100644 SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs delete mode 100644 SocketHttpListener/Net/HttpListenerResponse.Managed.cs delete mode 100644 SocketHttpListener/Net/HttpListenerResponse.cs delete mode 100644 SocketHttpListener/Net/HttpRequestStream.Managed.cs delete mode 100644 SocketHttpListener/Net/HttpRequestStream.cs delete mode 100644 SocketHttpListener/Net/HttpResponseStream.Managed.cs delete mode 100644 SocketHttpListener/Net/HttpResponseStream.cs delete mode 100644 SocketHttpListener/Net/HttpStatusCode.cs delete mode 100644 SocketHttpListener/Net/HttpStatusDescription.cs delete mode 100644 SocketHttpListener/Net/HttpStreamAsyncResult.cs delete mode 100644 SocketHttpListener/Net/HttpVersion.cs delete mode 100644 SocketHttpListener/Net/ListenerPrefix.cs delete mode 100644 SocketHttpListener/Net/UriScheme.cs delete mode 100644 SocketHttpListener/Net/WebHeaderCollection.cs delete mode 100644 SocketHttpListener/Net/WebHeaderEncoding.cs delete mode 100644 SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs delete mode 100644 SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs delete mode 100644 SocketHttpListener/Net/WebSockets/HttpWebSocket.cs delete mode 100644 SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs delete mode 100644 SocketHttpListener/Net/WebSockets/WebSocketContext.cs delete mode 100644 SocketHttpListener/Net/WebSockets/WebSocketValidate.cs diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 2e07281369..834a494f81 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.HttpClientManager } httpWebRequest.ContentType = contentType; - httpWebRequest.ContentLength = bytes.Length; + // httpWebRequest.ContentLength = bytes.Length; (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); } catch (Exception ex) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 7aedba9b31..835c6f52ef 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; @@ -14,6 +15,7 @@ namespace Emby.Server.Implementations.HttpServer public class FileWriter : IHttpResult { private ILogger Logger { get; set; } + public IFileSystem FileSystem { get; } private string RangeHeader { get; set; } private bool IsHeadRequest { get; set; } @@ -51,6 +53,7 @@ namespace Emby.Server.Implementations.HttpServer Path = path; Logger = logger; + FileSystem = fileSystem; RangeHeader = rangeHeader; Headers["Content-Type"] = contentType; @@ -60,7 +63,8 @@ namespace Emby.Server.Implementations.HttpServer if (string.IsNullOrWhiteSpace(rangeHeader)) { - Headers["Content-Length"] = TotalContentLength.ToString(UsCulture); + // TODO + //Headers["Content-Length"] = TotalContentLength.ToString(UsCulture); StatusCode = HttpStatusCode.OK; } else @@ -95,7 +99,7 @@ namespace Emby.Server.Implementations.HttpServer // Content-Length is the length of what we're serving, not the original content var lengthString = RangeLength.ToString(UsCulture); - Headers["Content-Length"] = lengthString; + // TODO Headers["Content-Length"] = lengthString; var rangeString = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); Headers["Content-Range"] = rangeString; @@ -174,12 +178,12 @@ namespace Emby.Server.Implementations.HttpServer } //var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0; - - await response.TransmitFile(path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false); + // TODO not DI friendly lol + await response.TransmitFile(path, 0, 0, FileShare, FileSystem, new StreamHelper(), cancellationToken).ConfigureAwait(false); return; } - - await response.TransmitFile(path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false); + // TODO not DI friendly lol + await response.TransmitFile(path, RangeStart, RangeLength, FileShare, FileSystem, new StreamHelper(), cancellationToken).ConfigureAwait(false); } finally { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index ee746c6696..fb42460f1b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -644,6 +644,9 @@ namespace Emby.Server.Implementations.HttpServer { var bOutput = Encoding.UTF8.GetBytes(text); response.SetContentLength(bOutput.Length); + // TODO + response.Headers.Remove("Content-Length"); // DO NOT SET THIS, IT'S DONE AUTOMATICALLY BECAUSE MS ARE NOT STUPID + response.SendChunked = true; return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 070717d489..5e9d2b4c36 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -592,7 +592,7 @@ namespace Emby.Server.Implementations.HttpServer { if (totalContentLength.HasValue) { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture); + // TODO responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture); } if (isHeadRequest) diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 891a76ec2a..9c8ab8d91c 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer RangeLength = 1 + RangeEnd - RangeStart; // Content-Length is the length of what we're serving, not the original content - Headers["Content-Length"] = RangeLength.ToString(UsCulture); + // TODO Headers["Content-Length"] = RangeLength.ToString(UsCulture); Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); if (RangeStart > 0 && SourceStream.CanSeek) diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index da2bf983a0..dbce3250d4 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.HttpServer public void FilterResponse(IRequest req, IResponse res, object dto) { // Try to prevent compatibility view - res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); + res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Origin", "*"); diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index cb2e3580b2..750b0f7958 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer if (source.CanSeek) { - Headers["Content-Length"] = source.Length.ToString(UsCulture); + // TODO Headers["Content-Length"] = source.Length.ToString(UsCulture); } } @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer Headers["Content-Type"] = contentType; - Headers["Content-Length"] = contentLength.ToString(UsCulture); + // TODO Headers["Content-Length"] = contentLength.ToString(UsCulture); } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 693c2328c6..64856f04af 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; + using System.Net; + using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; @@ -13,9 +14,8 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; -using SocketHttpListener.Net; -namespace Jellyfin.Server.SocketSharp + namespace Jellyfin.Server.SocketSharp { public class WebSocketSharpListener : IHttpListener { @@ -67,24 +67,44 @@ namespace Jellyfin.Server.SocketSharp public void Start(IEnumerable urlPrefixes) { + // TODO + //if (_listener == null) + //{ + // _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); + //} + + //_listener.EnableDualMode = _enableDualMode; + + //if (_certificate != null) + //{ + // _listener.LoadCert(_certificate); + //} + + //_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); + //_listener.Prefixes.AddRange(urlPrefixes); + + //_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); + + //_listener.Start(); + if (_listener == null) { - _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); + _listener = new HttpListener(); } - - _listener.EnableDualMode = _enableDualMode; - - if (_certificate != null) - { - _listener.LoadCert(_certificate); - } - + _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); - _listener.Prefixes.AddRange(urlPrefixes); - _listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); + //foreach (var urlPrefix in urlPrefixes) + //{ + // _listener.Prefixes.Add(urlPrefix); + //} + _listener.Prefixes.Add("http://localhost:8096/"); _listener.Start(); + + // TODO how to do this in netcore? + _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), + null); } private static void LogRequest(ILogger logger, HttpListenerRequest request) @@ -98,8 +118,10 @@ namespace Jellyfin.Server.SocketSharp request.UserAgent ?? string.Empty); } - private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken) + private Task InitTask(IAsyncResult asyncResult, CancellationToken cancellationToken) { + var context = _listener.EndGetContext(asyncResult); + _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), null); IHttpRequest httpReq = null; var request = context.Request; @@ -134,7 +156,7 @@ namespace Jellyfin.Server.SocketSharp var endpoint = ctx.Request.RemoteEndPoint.ToString(); var url = ctx.Request.RawUrl; - var queryString = ctx.Request.QueryString; + var queryString = new QueryParamCollection(ctx.Request.QueryString); var connectingArgs = new WebSocketConnectingEventArgs { @@ -153,7 +175,7 @@ namespace Jellyfin.Server.SocketSharp if (WebSocketConnected != null) { - var socket = new SharpWebSocket(webSocketContext.WebSocket, _logger); + SharpWebSocket socket = null; //new SharpWebSocket(webSocketContext.WebSocket, _logger); await socket.ConnectAsServerAsync().ConfigureAwait(false); WebSocketConnected(new WebSocketConnectEventArgs diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs index 6458707d92..6a00e283c6 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Net; using System.Text; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; @@ -438,7 +439,7 @@ namespace Jellyfin.Server.SocketSharp public string UserAgent => request.UserAgent; - public QueryParamCollection Headers => request.Headers; + public QueryParamCollection Headers => new QueryParamCollection(request.Headers); private QueryParamCollection queryString; public QueryParamCollection QueryString => queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs index cf5aee5d40..552ae1754d 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; -using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; using IRequest = MediaBrowser.Model.Services.IRequest; @@ -53,7 +54,7 @@ namespace Jellyfin.Server.SocketSharp set => _response.ContentType = value; } - public QueryParamCollection Headers => _response.Headers; + public QueryParamCollection Headers => new QueryParamCollection(_response.Headers); private static string AsHeaderValue(Cookie cookie) { @@ -152,7 +153,7 @@ namespace Jellyfin.Server.SocketSharp // you can happily set the Content-Length header in Asp.Net // but HttpListener will complain if you do - you have to set ContentLength64 on the response. // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header - _response.ContentLength64 = contentLength; + //_response.ContentLength64 = contentLength; } public void SetCookie(Cookie cookie) @@ -172,10 +173,43 @@ namespace Jellyfin.Server.SocketSharp public void ClearCookies() { } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + const int StreamCopyToBufferSize = 81920; + public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) { - return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); + // TODO + // return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); + var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + //if (count <= 0) + //{ + // allowAsync = true; + //} + + var fileOpenOptions = FileOpenOptions.SequentialScan; + + if (allowAsync) + { + fileOpenOptions |= FileOpenOptions.Asynchronous; + } + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + + using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + { + if (offset > 0) + { + fs.Position = offset; + } + + if (count > 0) + { + await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } } } } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 6a98c5e8a6..3e74d59db7 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -324,7 +324,8 @@ namespace MediaBrowser.Api.Playback.Progressive // Seeing cases of -1 here if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) { - responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); + // TODO + //responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); } if (isHeadRequest) @@ -382,17 +383,18 @@ namespace MediaBrowser.Api.Playback.Progressive var hasHeaders = streamResult as IHasHeaders; if (hasHeaders != null) { - if (contentLength.HasValue) - { - hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture); - } - else - { + // TODO + //if (contentLength.HasValue) + //{ + // hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture); + //} + //else + //{ if (hasHeaders.Headers.ContainsKey("Content-Length")) { hasHeaders.Headers.Remove("Content-Length"); } - } + //} } return streamResult; diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index ac9b981b98..909cebce3d 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -152,7 +152,7 @@ namespace MediaBrowser.Model.Services QueryParamCollection Headers { get; } - Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken); + Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken); bool SendChunked { get; set; } } diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 4297b97c66..0e0ebf848b 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; +using System.Net; using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Services { + // Remove this garbage class, it's just a bastard copy of NameValueCollection public class QueryParamCollection : List { public QueryParamCollection() @@ -20,6 +23,30 @@ namespace MediaBrowser.Model.Services } } + // TODO remove this shit + public QueryParamCollection(WebHeaderCollection webHeaderCollection) + { + foreach (var key in webHeaderCollection.AllKeys) + { + foreach (var value in webHeaderCollection.GetValues(key) ?? Array.Empty()) + { + Add(key, value); + } + } + } + + // TODO remove this shit + public QueryParamCollection(NameValueCollection nameValueCollection) + { + foreach (var key in nameValueCollection.AllKeys) + { + foreach (var value in nameValueCollection.GetValues(key) ?? Array.Empty()) + { + Add(key, value); + } + } + } + private static StringComparison GetStringComparison() { return StringComparison.OrdinalIgnoreCase; @@ -50,6 +77,10 @@ namespace MediaBrowser.Model.Services /// public virtual void Add(string key, string value) { + if (string.Equals(key, "content-length", StringComparison.OrdinalIgnoreCase)) + { + return; + } Add(new NameValuePair(key, value)); } diff --git a/SocketHttpListener/Ext.cs b/SocketHttpListener/Ext.cs index 2b3c67071c..697c5d5b70 100644 --- a/SocketHttpListener/Ext.cs +++ b/SocketHttpListener/Ext.cs @@ -6,7 +6,6 @@ using System.Net; using System.Text; using System.Threading.Tasks; using MediaBrowser.Model.Services; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; using WebSocketState = System.Net.WebSockets.WebSocketState; namespace SocketHttpListener diff --git a/SocketHttpListener/HttpResponse.cs b/SocketHttpListener/HttpResponse.cs index d5d9b4a1c8..a33b942959 100644 --- a/SocketHttpListener/HttpResponse.cs +++ b/SocketHttpListener/HttpResponse.cs @@ -1,123 +1,122 @@ -using System; -using System.Linq; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; -using HttpVersion = SocketHttpListener.Net.HttpVersion; - -namespace SocketHttpListener +using System; +using System.Linq; +using System.Net; +using System.Text; +using MediaBrowser.Model.Services; +using SocketHttpListener.Net; + +namespace SocketHttpListener { - internal class HttpResponse : HttpBase - { - #region Private Fields - - private string _code; - private string _reason; - - #endregion - - #region Private Constructors - - private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) - : base(version, headers) - { - _code = code; - _reason = reason; - } - - #endregion - - #region Internal Constructors - - internal HttpResponse(HttpStatusCode code) - : this(code, code.GetDescription()) - { - } - - internal HttpResponse(HttpStatusCode code, string reason) - : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) - { - Headers["Server"] = "websocket-sharp/1.0"; - } - - #endregion - - #region Public Properties - - public CookieCollection Cookies => GetCookies(Headers, true); - - private static CookieCollection GetCookies(QueryParamCollection headers, bool response) - { - var name = response ? "Set-Cookie" : "Cookie"; - return headers == null || !headers.Contains(name) - ? new CookieCollection() - : CookieHelper.Parse(headers[name], response); - } - - public bool IsProxyAuthenticationRequired => _code == "407"; - - public bool IsUnauthorized => _code == "401"; - - public bool IsWebSocketResponse - { - get - { - var headers = Headers; - return ProtocolVersion > HttpVersion.Version10 && - _code == "101" && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); - } - } - - public string Reason => _reason; - - public string StatusCode => _code; - - #endregion - - #region Internal Methods - - internal static HttpResponse CreateCloseResponse(HttpStatusCode code) - { - var res = new HttpResponse(code); - res.Headers["Connection"] = "close"; - - return res; - } - - #endregion - - #region Public Methods - - public void SetCookies(CookieCollection cookies) - { - if (cookies == null || cookies.Count == 0) - return; - - var headers = Headers; - var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); - - foreach (var cookie in sorted) - headers.Add("Set-Cookie", cookie.ToString()); - } - - public override string ToString() - { - var output = new StringBuilder(64); - output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); - - var headers = Headers; - foreach (var key in headers.Keys) - output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); - - output.Append(CrLf); - - return output.ToString(); - } - - #endregion - } -} + // TODO what is the point of this class? + internal class HttpResponse : HttpBase + { + #region Private Fields + + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) + : base(version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse(HttpStatusCode code) + : this(code, code.GetDescription()) + { + } + + internal HttpResponse(HttpStatusCode code, string reason) + : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public CookieCollection Cookies => GetCookies(Headers, true); + + private static CookieCollection GetCookies(QueryParamCollection headers, bool response) + { + var name = response ? "Set-Cookie" : "Cookie"; + return headers == null || !headers.Contains(name) + ? new CookieCollection() + : CookieHelper.Parse(headers[name], response); + } + + public bool IsProxyAuthenticationRequired => _code == "407"; + + public bool IsUnauthorized => _code == "401"; + + public bool IsWebSocketResponse + { + get + { + var headers = Headers; + return ProtocolVersion > HttpVersion.Version10 && + _code == "101" && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + } + } + + public string Reason => _reason; + + public string StatusCode => _code; + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse(HttpStatusCode code) + { + var res = new HttpResponse(code); + res.Headers["Connection"] = "close"; + + return res; + } + + #endregion + + #region Public Methods + + public void SetCookies(CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var headers = Headers; + var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); + + foreach (var cookie in sorted) + headers.Add("Set-Cookie", cookie.ToString()); + } + + public override string ToString() + { + var output = new StringBuilder(64); + output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + + var headers = Headers; + foreach (var key in headers.Keys) + output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); + + output.Append(CrLf); + + return output.ToString(); + } + + #endregion + } +} diff --git a/SocketHttpListener/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener/Net/AuthenticationSchemeSelector.cs deleted file mode 100644 index 77e6d0b8ab..0000000000 --- a/SocketHttpListener/Net/AuthenticationSchemeSelector.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Net; - -namespace SocketHttpListener.Net -{ - public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest); -} diff --git a/SocketHttpListener/Net/AuthenticationTypes.cs b/SocketHttpListener/Net/AuthenticationTypes.cs deleted file mode 100644 index a3dbe9dfff..0000000000 --- a/SocketHttpListener/Net/AuthenticationTypes.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal class AuthenticationTypes - { - internal const string NTLM = "NTLM"; - internal const string Negotiate = "Negotiate"; - internal const string Basic = "Basic"; - } -} diff --git a/SocketHttpListener/Net/BoundaryType.cs b/SocketHttpListener/Net/BoundaryType.cs deleted file mode 100644 index da0b22910e..0000000000 --- a/SocketHttpListener/Net/BoundaryType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal enum BoundaryType - { - ContentLength = 0, // Content-Length: XXX - Chunked = 1, // Transfer-Encoding: chunked - Multipart = 3, - None = 4, - Invalid = 5, - } -} diff --git a/SocketHttpListener/Net/ChunkStream.cs b/SocketHttpListener/Net/ChunkStream.cs deleted file mode 100644 index 3836947d4c..0000000000 --- a/SocketHttpListener/Net/ChunkStream.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal sealed class ChunkStream - { - private enum State - { - None, - PartialSize, - Body, - BodyFinished, - Trailer - } - - private class Chunk - { - public byte[] Bytes; - public int Offset; - - public Chunk(byte[] chunk) - { - Bytes = chunk; - } - - public int Read(byte[] buffer, int offset, int size) - { - int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; - Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); - Offset += nread; - return nread; - } - } - - internal WebHeaderCollection _headers; - private int _chunkSize; - private int _chunkRead; - private int _totalWritten; - private State _state; - private StringBuilder _saved; - private bool _sawCR; - private bool _gotit; - private int _trailerState; - private List _chunks; - - public ChunkStream(WebHeaderCollection headers) - { - _headers = headers; - _saved = new StringBuilder(); - _chunks = new List(); - _chunkSize = -1; - _totalWritten = 0; - } - - public void ResetBuffer() - { - _chunkSize = -1; - _chunkRead = 0; - _totalWritten = 0; - _chunks.Clear(); - } - - public int Read(byte[] buffer, int offset, int size) - { - return ReadFromChunks(buffer, offset, size); - } - - private int ReadFromChunks(byte[] buffer, int offset, int size) - { - int count = _chunks.Count; - int nread = 0; - - var chunksForRemoving = new List(count); - for (int i = 0; i < count; i++) - { - var chunk = _chunks[i]; - - if (chunk.Offset == chunk.Bytes.Length) - { - chunksForRemoving.Add(chunk); - continue; - } - - nread += chunk.Read(buffer, offset + nread, size - nread); - if (nread == size) - break; - } - - foreach (var chunk in chunksForRemoving) - _chunks.Remove(chunk); - - return nread; - } - - public void Write(byte[] buffer, int offset, int size) - { - // Note, the logic here only works when offset is 0 here. - // Otherwise, it would treat "size" as the end offset instead of an actual byte count from offset. - - if (offset < size) - InternalWrite(buffer, ref offset, size); - } - - private void InternalWrite(byte[] buffer, ref int offset, int size) - { - if (_state == State.None || _state == State.PartialSize) - { - _state = GetChunkSize(buffer, ref offset, size); - if (_state == State.PartialSize) - return; - - _saved.Length = 0; - _sawCR = false; - _gotit = false; - } - - if (_state == State.Body && offset < size) - { - _state = ReadBody(buffer, ref offset, size); - if (_state == State.Body) - return; - } - - if (_state == State.BodyFinished && offset < size) - { - _state = ReadCRLF(buffer, ref offset, size); - if (_state == State.BodyFinished) - return; - - _sawCR = false; - } - - if (_state == State.Trailer && offset < size) - { - _state = ReadTrailer(buffer, ref offset, size); - if (_state == State.Trailer) - return; - - _saved.Length = 0; - _sawCR = false; - _gotit = false; - } - - if (offset < size) - InternalWrite(buffer, ref offset, size); - } - - public bool WantMore => (_chunkRead != _chunkSize || _chunkSize != 0 || _state != State.None); - - public bool DataAvailable - { - get - { - int count = _chunks.Count; - for (int i = 0; i < count; i++) - { - var ch = _chunks[i]; - if (ch == null || ch.Bytes == null) - continue; - if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) - return (_state != State.Body); - } - return false; - } - } - - public int TotalDataSize => _totalWritten; - - public int ChunkLeft => _chunkSize - _chunkRead; - - private State ReadBody(byte[] buffer, ref int offset, int size) - { - if (_chunkSize == 0) - return State.BodyFinished; - - int diff = size - offset; - if (diff + _chunkRead > _chunkSize) - diff = _chunkSize - _chunkRead; - - byte[] chunk = new byte[diff]; - Buffer.BlockCopy(buffer, offset, chunk, 0, diff); - _chunks.Add(new Chunk(chunk)); - offset += diff; - _chunkRead += diff; - _totalWritten += diff; - - return (_chunkRead == _chunkSize) ? State.BodyFinished : State.Body; - } - - private State GetChunkSize(byte[] buffer, ref int offset, int size) - { - _chunkRead = 0; - _chunkSize = 0; - char c = '\0'; - while (offset < size) - { - c = (char)buffer[offset++]; - if (c == '\r') - { - if (_sawCR) - ThrowProtocolViolation("2 CR found"); - - _sawCR = true; - continue; - } - - if (_sawCR && c == '\n') - break; - - if (c == ' ') - _gotit = true; - - if (!_gotit) - _saved.Append(c); - - if (_saved.Length > 20) - ThrowProtocolViolation("chunk size too long."); - } - - if (!_sawCR || c != '\n') - { - if (offset < size) - ThrowProtocolViolation("Missing \\n"); - - try - { - if (_saved.Length > 0) - { - _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber); - } - } - catch (Exception) - { - ThrowProtocolViolation("Cannot parse chunk size."); - } - - return State.PartialSize; - } - - _chunkRead = 0; - try - { - _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber); - } - catch (Exception) - { - ThrowProtocolViolation("Cannot parse chunk size."); - } - - if (_chunkSize == 0) - { - _trailerState = 2; - return State.Trailer; - } - - return State.Body; - } - - private static string RemoveChunkExtension(string input) - { - int idx = input.IndexOf(';'); - if (idx == -1) - return input; - return input.Substring(0, idx); - } - - private State ReadCRLF(byte[] buffer, ref int offset, int size) - { - if (!_sawCR) - { - if ((char)buffer[offset++] != '\r') - ThrowProtocolViolation("Expecting \\r"); - - _sawCR = true; - if (offset == size) - return State.BodyFinished; - } - - if (_sawCR && (char)buffer[offset++] != '\n') - ThrowProtocolViolation("Expecting \\n"); - - return State.None; - } - - private State ReadTrailer(byte[] buffer, ref int offset, int size) - { - char c = '\0'; - - // short path - if (_trailerState == 2 && (char)buffer[offset] == '\r' && _saved.Length == 0) - { - offset++; - if (offset < size && (char)buffer[offset] == '\n') - { - offset++; - return State.None; - } - offset--; - } - - int st = _trailerState; - string stString = "\r\n\r"; - while (offset < size && st < 4) - { - c = (char)buffer[offset++]; - if ((st == 0 || st == 2) && c == '\r') - { - st++; - continue; - } - - if ((st == 1 || st == 3) && c == '\n') - { - st++; - continue; - } - - if (st > 0) - { - _saved.Append(stString.Substring(0, _saved.Length == 0 ? st - 2 : st)); - st = 0; - if (_saved.Length > 4196) - ThrowProtocolViolation("Error reading trailer (too long)."); - } - } - - if (st < 4) - { - _trailerState = st; - if (offset < size) - ThrowProtocolViolation("Error reading trailer."); - - return State.Trailer; - } - - var reader = new StringReader(_saved.ToString()); - string line; - while ((line = reader.ReadLine()) != null && line != "") - _headers.Add(line); - - return State.None; - } - - private static void ThrowProtocolViolation(string message) - { - var we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); - throw we; - } - } -} diff --git a/SocketHttpListener/Net/ChunkedInputStream.cs b/SocketHttpListener/Net/ChunkedInputStream.cs deleted file mode 100644 index 7aa8529e38..0000000000 --- a/SocketHttpListener/Net/ChunkedInputStream.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; -using System.Net; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal sealed class ChunkedInputStream : HttpRequestStream - { - private ChunkStream _decoder; - private readonly HttpListenerContext _context; - private bool _no_more_data; - - private class ReadBufferState - { - public byte[] Buffer; - public int Offset; - public int Count; - public int InitialCount; - public HttpStreamAsyncResult Ares; - public ReadBufferState(byte[] buffer, int offset, int count, HttpStreamAsyncResult ares) - { - Buffer = buffer; - Offset = offset; - Count = count; - InitialCount = count; - Ares = ares; - } - } - - public ChunkedInputStream(HttpListenerContext context, Stream stream, byte[] buffer, int offset, int length) - : base(stream, buffer, offset, length) - { - _context = context; - var coll = (WebHeaderCollection)context.Request.Headers; - _decoder = new ChunkStream(coll); - } - - public ChunkStream Decoder - { - get => _decoder; - set => _decoder = value; - } - - protected override int ReadCore(byte[] buffer, int offset, int count) - { - IAsyncResult ares = BeginReadCore(buffer, offset, count, null, null); - return EndRead(ares); - } - - protected override IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - if (_no_more_data || size == 0 || _closed) - { - ares.Complete(); - return ares; - } - int nread = _decoder.Read(buffer, offset, size); - offset += nread; - size -= nread; - if (size == 0) - { - // got all we wanted, no need to bother the decoder yet - ares._count = nread; - ares.Complete(); - return ares; - } - if (!_decoder.WantMore) - { - _no_more_data = nread == 0; - ares._count = nread; - ares.Complete(); - return ares; - } - ares._buffer = new byte[8192]; - ares._offset = 0; - ares._count = 8192; - var rb = new ReadBufferState(buffer, offset, size, ares); - rb.InitialCount += nread; - base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb); - return ares; - } - - private void OnRead(IAsyncResult base_ares) - { - ReadBufferState rb = (ReadBufferState)base_ares.AsyncState; - var ares = rb.Ares; - try - { - int nread = base.EndRead(base_ares); - if (nread == 0) - { - _no_more_data = true; - ares._count = rb.InitialCount - rb.Count; - ares.Complete(); - return; - } - - _decoder.Write(ares._buffer, ares._offset, nread); - nread = _decoder.Read(rb.Buffer, rb.Offset, rb.Count); - rb.Offset += nread; - rb.Count -= nread; - if (rb.Count == 0 || !_decoder.WantMore) - { - _no_more_data = !_decoder.WantMore && nread == 0; - ares._count = rb.InitialCount - rb.Count; - ares.Complete(); - return; - } - ares._offset = 0; - ares._count = Math.Min(8192, _decoder.ChunkLeft + 6); - base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb); - } - catch (Exception e) - { - _context.Connection.SendError(e.Message, 400); - ares.Complete(e); - } - } - - public override int EndRead(IAsyncResult asyncResult) - { - if (asyncResult == null) - throw new ArgumentNullException(nameof(asyncResult)); - - var ares = asyncResult as HttpStreamAsyncResult; - if (ares == null || !ReferenceEquals(this, ares._parent)) - { - throw new ArgumentException("Invalid async result"); - } - if (ares._endCalled) - { - throw new InvalidOperationException("Invalid end call"); - } - ares._endCalled = true; - - if (!asyncResult.IsCompleted) - asyncResult.AsyncWaitHandle.WaitOne(); - - if (ares._error != null) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "Operation aborted"); - - return ares._count; - } - } -} diff --git a/SocketHttpListener/Net/EntitySendFormat.cs b/SocketHttpListener/Net/EntitySendFormat.cs deleted file mode 100644 index 3ed981084d..0000000000 --- a/SocketHttpListener/Net/EntitySendFormat.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal enum EntitySendFormat - { - ContentLength = 0, // Content-Length: XXX - Chunked = 1, // Transfer-Encoding: chunked - } -} diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs deleted file mode 100644 index e89f4ed9b1..0000000000 --- a/SocketHttpListener/Net/HttpConnection.cs +++ /dev/null @@ -1,528 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; -namespace SocketHttpListener.Net -{ - sealed class HttpConnection - { - private static AsyncCallback s_onreadCallback = new AsyncCallback(OnRead); - const int BufferSize = 8192; - Socket _socket; - Stream _stream; - HttpEndPointListener _epl; - MemoryStream _memoryStream; - byte[] _buffer; - HttpListenerContext _context; - StringBuilder _currentLine; - ListenerPrefix _prefix; - HttpRequestStream _requestStream; - HttpResponseStream _responseStream; - bool _chunked; - int _reuses; - bool _contextBound; - bool secure; - IPEndPoint local_ep; - HttpListener _lastListener; - X509Certificate cert; - SslStream ssl_stream; - - private readonly ILogger _logger; - private readonly ICryptoProvider _cryptoProvider; - private readonly IStreamHelper _streamHelper; - private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environment; - - public HttpConnection(ILogger logger, Socket socket, HttpEndPointListener epl, bool secure, - X509Certificate cert, ICryptoProvider cryptoProvider, IStreamHelper streamHelper, IFileSystem fileSystem, - IEnvironmentInfo environment) - { - _logger = logger; - this._socket = socket; - this._epl = epl; - this.secure = secure; - this.cert = cert; - _cryptoProvider = cryptoProvider; - _streamHelper = streamHelper; - _fileSystem = fileSystem; - _environment = environment; - - if (secure == false) - { - _stream = new SocketStream(_socket, false); - } - else - { - ssl_stream = new SslStream(new SocketStream(_socket, false), false, (t, c, ch, e) => - { - if (c == null) - { - return true; - } - - //var c2 = c as X509Certificate2; - //if (c2 == null) - //{ - // c2 = new X509Certificate2(c.GetRawCertData()); - //} - - //_clientCert = c2; - //_clientCertErrors = new int[] { (int)e }; - return true; - }); - - _stream = ssl_stream; - } - } - - public Stream Stream => _stream; - - public async Task Init() - { - if (ssl_stream != null) - { - var enableAsync = true; - if (enableAsync) - { - await ssl_stream.AuthenticateAsServerAsync(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false).ConfigureAwait(false); - } - else - { - ssl_stream.AuthenticateAsServer(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false); - } - } - - InitInternal(); - } - - private void InitInternal() - { - _contextBound = false; - _requestStream = null; - _responseStream = null; - _prefix = null; - _chunked = false; - _memoryStream = new MemoryStream(); - _position = 0; - _inputState = InputState.RequestLine; - _lineState = LineState.None; - _context = new HttpListenerContext(this); - } - - public bool IsClosed => (_socket == null); - - public int Reuses => _reuses; - - public IPEndPoint LocalEndPoint - { - get - { - if (local_ep != null) - return local_ep; - - local_ep = (IPEndPoint)_socket.LocalEndPoint; - return local_ep; - } - } - - public IPEndPoint RemoteEndPoint => _socket.RemoteEndPoint as IPEndPoint; - - public bool IsSecure => secure; - - public ListenerPrefix Prefix - { - get => _prefix; - set => _prefix = value; - } - - private void OnTimeout(object unused) - { - //_logger.LogInformation("HttpConnection timer fired"); - CloseSocket(); - Unbind(); - } - - public void BeginReadRequest() - { - if (_buffer == null) - _buffer = new byte[BufferSize]; - try - { - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - catch - { - CloseSocket(); - Unbind(); - } - } - - public HttpRequestStream GetRequestStream(bool chunked, long contentlength) - { - if (_requestStream == null) - { - byte[] buffer = _memoryStream.GetBuffer(); - int length = (int)_memoryStream.Length; - _memoryStream = null; - if (chunked) - { - _chunked = true; - //_context.Response.SendChunked = true; - _requestStream = new ChunkedInputStream(_context, _stream, buffer, _position, length - _position); - } - else - { - _requestStream = new HttpRequestStream(_stream, buffer, _position, length - _position, contentlength); - } - } - return _requestStream; - } - - public HttpResponseStream GetResponseStream(bool isExpect100Continue = false) - { - // TODO: can we get this _stream before reading the input? - if (_responseStream == null) - { - var supportsDirectSocketAccess = !_context.Response.SendChunked && !isExpect100Continue && !secure; - - _responseStream = new HttpResponseStream(_stream, _context.Response, false, _streamHelper, _socket, supportsDirectSocketAccess, _environment, _fileSystem, _logger); - } - return _responseStream; - } - - private static void OnRead(IAsyncResult ares) - { - var cnc = (HttpConnection)ares.AsyncState; - cnc.OnReadInternal(ares); - } - - private void OnReadInternal(IAsyncResult ares) - { - int nread = -1; - try - { - nread = _stream.EndRead(ares); - _memoryStream.Write(_buffer, 0, nread); - if (_memoryStream.Length > 32768) - { - SendError("Bad Request", 400); - Close(true); - return; - } - } - catch - { - if (_memoryStream != null && _memoryStream.Length > 0) - SendError(); - if (_socket != null) - { - CloseSocket(); - Unbind(); - } - return; - } - - if (nread == 0) - { - CloseSocket(); - Unbind(); - return; - } - - if (ProcessInput(_memoryStream)) - { - if (!_context.HaveError) - _context.Request.FinishInitialization(); - - if (_context.HaveError) - { - SendError(); - Close(true); - return; - } - - if (!_epl.BindContext(_context)) - { - const int NotFoundErrorCode = 404; - SendError(HttpStatusDescription.Get(NotFoundErrorCode), NotFoundErrorCode); - Close(true); - return; - } - HttpListener listener = _epl.Listener; - if (_lastListener != listener) - { - RemoveConnection(); - listener.AddConnection(this); - _lastListener = listener; - } - - _contextBound = true; - listener.RegisterContext(_context); - return; - } - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - - private void RemoveConnection() - { - if (_lastListener == null) - _epl.RemoveConnection(this); - else - _lastListener.RemoveConnection(this); - } - - private enum InputState - { - RequestLine, - Headers - } - - private enum LineState - { - None, - CR, - LF - } - - InputState _inputState = InputState.RequestLine; - LineState _lineState = LineState.None; - int _position; - - // true -> done processing - // false -> need more input - private bool ProcessInput(MemoryStream ms) - { - byte[] buffer = ms.GetBuffer(); - int len = (int)ms.Length; - int used = 0; - string line; - - while (true) - { - if (_context.HaveError) - return true; - - if (_position >= len) - break; - - try - { - line = ReadLine(buffer, _position, len - _position, ref used); - _position += used; - } - catch - { - _context.ErrorMessage = "Bad request"; - _context.ErrorStatus = 400; - return true; - } - - if (line == null) - break; - - if (line == "") - { - if (_inputState == InputState.RequestLine) - continue; - _currentLine = null; - ms = null; - return true; - } - - if (_inputState == InputState.RequestLine) - { - _context.Request.SetRequestLine(line); - _inputState = InputState.Headers; - } - else - { - try - { - _context.Request.AddHeader(line); - } - catch (Exception e) - { - _context.ErrorMessage = e.Message; - _context.ErrorStatus = 400; - return true; - } - } - } - - if (used == len) - { - ms.SetLength(0); - _position = 0; - } - return false; - } - - private string ReadLine(byte[] buffer, int offset, int len, ref int used) - { - if (_currentLine == null) - _currentLine = new StringBuilder(128); - int last = offset + len; - used = 0; - for (int i = offset; i < last && _lineState != LineState.LF; i++) - { - used++; - byte b = buffer[i]; - if (b == 13) - { - _lineState = LineState.CR; - } - else if (b == 10) - { - _lineState = LineState.LF; - } - else - { - _currentLine.Append((char)b); - } - } - - string result = null; - if (_lineState == LineState.LF) - { - _lineState = LineState.None; - result = _currentLine.ToString(); - _currentLine.Length = 0; - } - - return result; - } - - public void SendError(string msg, int status) - { - try - { - HttpListenerResponse response = _context.Response; - response.StatusCode = status; - response.ContentType = "text/html"; - string description = HttpStatusDescription.Get(status); - string str; - if (msg != null) - str = string.Format("

{0} ({1})

", description, msg); - else - str = string.Format("

{0}

", description); - - byte[] error = Encoding.UTF8.GetBytes(str); - response.Close(error, false); - } - catch - { - // response was already closed - } - } - - public void SendError() - { - SendError(_context.ErrorMessage, _context.ErrorStatus); - } - - private void Unbind() - { - if (_contextBound) - { - _epl.UnbindContext(_context); - _contextBound = false; - } - } - - public void Close() - { - Close(false); - } - - private void CloseSocket() - { - if (_socket == null) - return; - - try - { - _socket.Close(); - } - catch { } - finally - { - _socket = null; - } - - RemoveConnection(); - } - - internal void Close(bool force) - { - if (_socket != null) - { - Stream st = GetResponseStream(); - if (st != null) - st.Close(); - - _responseStream = null; - } - - if (_socket != null) - { - force |= !_context.Request.KeepAlive; - if (!force) - force = (string.Equals(_context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase)); - - if (!force && _context.Request.FlushInput()) - { - if (_chunked && _context.Response.ForceCloseChunked == false) - { - // Don't close. Keep working. - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - Socket s = _socket; - _socket = null; - try - { - if (s != null) - s.Shutdown(SocketShutdown.Both); - } - catch - { - } - finally - { - if (s != null) - { - try - { - s.Close(); - } - catch { } - } - } - Unbind(); - RemoveConnection(); - return; - } - } - } -} diff --git a/SocketHttpListener/Net/HttpEndPointListener.cs b/SocketHttpListener/Net/HttpEndPointListener.cs deleted file mode 100644 index 35d1af6486..0000000000 --- a/SocketHttpListener/Net/HttpEndPointListener.cs +++ /dev/null @@ -1,526 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - internal sealed class HttpEndPointListener - { - private HttpListener _listener; - private IPEndPoint _endpoint; - private Socket _socket; - private Dictionary _prefixes; - private List _unhandledPrefixes; // host = '*' - private List _allPrefixes; // host = '+' - private X509Certificate _cert; - private bool _secure; - private Dictionary _unregisteredConnections; - - private readonly ILogger _logger; - private bool _closed; - private bool _enableDualMode; - private readonly ICryptoProvider _cryptoProvider; - private readonly ISocketFactory _socketFactory; - private readonly IStreamHelper _streamHelper; - private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environment; - - public HttpEndPointListener(HttpListener listener, IPAddress addr, int port, bool secure, X509Certificate cert, - ILogger logger, ICryptoProvider cryptoProvider, ISocketFactory socketFactory, IStreamHelper streamHelper, - IFileSystem fileSystem, IEnvironmentInfo environment) - { - this._listener = listener; - _logger = logger; - _cryptoProvider = cryptoProvider; - _socketFactory = socketFactory; - _streamHelper = streamHelper; - _fileSystem = fileSystem; - _environment = environment; - - this._secure = secure; - this._cert = cert; - - _enableDualMode = addr.Equals(IPAddress.IPv6Any); - _endpoint = new IPEndPoint(addr, port); - - _prefixes = new Dictionary(); - _unregisteredConnections = new Dictionary(); - - CreateSocket(); - } - - internal HttpListener Listener => _listener; - - private void CreateSocket() - { - try - { - _socket = CreateSocket(_endpoint.Address.AddressFamily, _enableDualMode); - } - catch (SocketCreateException ex) - { - if (_enableDualMode && _endpoint.Address.Equals(IPAddress.IPv6Any) && - (string.Equals(ex.ErrorCode, "AddressFamilyNotSupported", StringComparison.OrdinalIgnoreCase) || - // mono 4.8.1 and lower on bsd is throwing this - string.Equals(ex.ErrorCode, "ProtocolNotSupported", StringComparison.OrdinalIgnoreCase) || - // mono 5.2 on bsd is throwing this - string.Equals(ex.ErrorCode, "OperationNotSupported", StringComparison.OrdinalIgnoreCase))) - { - _endpoint = new IPEndPoint(IPAddress.Any, _endpoint.Port); - _enableDualMode = false; - _socket = CreateSocket(_endpoint.Address.AddressFamily, _enableDualMode); - } - else - { - throw; - } - } - - try - { - _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (SocketException) - { - // This is not supported on all operating systems (qnap) - } - - _socket.Bind(_endpoint); - - // This is the number TcpListener uses. - _socket.Listen(2147483647); - - Accept(); - - _closed = false; - } - - private void Accept() - { - var acceptEventArg = new SocketAsyncEventArgs(); - acceptEventArg.UserToken = this; - acceptEventArg.Completed += new EventHandler(OnAccept); - - Accept(acceptEventArg); - } - - private static void TryCloseAndDispose(Socket socket) - { - try - { - using (socket) - { - socket.Close(); - } - } - catch - { - - } - } - - private static void TryClose(Socket socket) - { - try - { - socket.Close(); - } - catch - { - - } - } - - private void Accept(SocketAsyncEventArgs acceptEventArg) - { - // acceptSocket must be cleared since the context object is being reused - acceptEventArg.AcceptSocket = null; - - try - { - bool willRaiseEvent = _socket.AcceptAsync(acceptEventArg); - - if (!willRaiseEvent) - { - ProcessAccept(acceptEventArg); - } - } - catch (ObjectDisposedException) - { - // TODO Investigate or properly fix. - } - catch (Exception ex) - { - var epl = (HttpEndPointListener)acceptEventArg.UserToken; - - epl._logger.LogError(ex, "Error in socket.AcceptAsync"); - } - } - - // This method is the callback method associated with Socket.AcceptAsync - // operations and is invoked when an accept operation is complete - // - private static void OnAccept(object sender, SocketAsyncEventArgs e) - { - ProcessAccept(e); - } - - private static async void ProcessAccept(SocketAsyncEventArgs args) - { - var epl = (HttpEndPointListener)args.UserToken; - - if (epl._closed) - { - return; - } - - // http://msdn.microsoft.com/en-us/library/system.net.sockets.acceptSocket.acceptasync%28v=vs.110%29.aspx - // Under certain conditions ConnectionReset can occur - // Need to attept to re-accept - var socketError = args.SocketError; - var accepted = args.AcceptSocket; - - epl.Accept(args); - - if (socketError == SocketError.ConnectionReset) - { - epl._logger.LogError("SocketError.ConnectionReset reported. Attempting to re-accept."); - return; - } - - if (accepted == null) - { - return; - } - - if (epl._secure && epl._cert == null) - { - TryClose(accepted); - return; - } - - try - { - var remoteEndPointString = accepted.RemoteEndPoint == null ? string.Empty : accepted.RemoteEndPoint.ToString(); - var localEndPointString = accepted.LocalEndPoint == null ? string.Empty : accepted.LocalEndPoint.ToString(); - //_logger.LogInformation("HttpEndPointListener Accepting connection from {0} to {1} secure connection requested: {2}", remoteEndPointString, localEndPointString, _secure); - - var conn = new HttpConnection(epl._logger, accepted, epl, epl._secure, epl._cert, epl._cryptoProvider, epl._streamHelper, epl._fileSystem, epl._environment); - - await conn.Init().ConfigureAwait(false); - - //_logger.LogDebug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); - lock (epl._unregisteredConnections) - { - epl._unregisteredConnections[conn] = conn; - } - conn.BeginReadRequest(); - } - catch (Exception ex) - { - epl._logger.LogError(ex, "Error in ProcessAccept"); - - TryClose(accepted); - epl.Accept(); - return; - } - } - - private Socket CreateSocket(AddressFamily addressFamily, bool dualMode) - { - try - { - var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - - if (dualMode) - { - socket.DualMode = true; - } - - return socket; - } - catch (SocketException ex) - { - throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); - } - catch (ArgumentException ex) - { - if (dualMode) - { - // Mono for BSD incorrectly throws ArgumentException instead of SocketException - throw new SocketCreateException("AddressFamilyNotSupported", ex); - } - else - { - throw; - } - } - } - - internal void RemoveConnection(HttpConnection conn) - { - lock (_unregisteredConnections) - { - _unregisteredConnections.Remove(conn); - } - } - - public bool BindContext(HttpListenerContext context) - { - var req = context.Request; - HttpListener listener = SearchListener(req.Url, out var prefix); - if (listener == null) - return false; - - context.Connection.Prefix = prefix; - return true; - } - - public void UnbindContext(HttpListenerContext context) - { - if (context == null || context.Request == null) - return; - - _listener.UnregisterContext(context); - } - - private HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) - { - prefix = null; - if (uri == null) - return null; - - string host = uri.Host; - int port = uri.Port; - string path = WebUtility.UrlDecode(uri.AbsolutePath); - string pathSlash = path[path.Length - 1] == '/' ? path : path + "/"; - - HttpListener bestMatch = null; - int bestLength = -1; - - if (host != null && host != "") - { - Dictionary localPrefixes = _prefixes; - foreach (var p in localPrefixes.Keys) - { - string ppath = p.Path; - if (ppath.Length < bestLength) - continue; - - if (p.Host != host || p.Port != port) - continue; - - if (path.StartsWith(ppath) || pathSlash.StartsWith(ppath)) - { - bestLength = ppath.Length; - bestMatch = localPrefixes[p]; - prefix = p; - } - } - if (bestLength != -1) - return bestMatch; - } - - List list = _unhandledPrefixes; - bestMatch = MatchFromList(host, path, list, out prefix); - - if (path != pathSlash && bestMatch == null) - bestMatch = MatchFromList(host, pathSlash, list, out prefix); - - if (bestMatch != null) - return bestMatch; - - list = _allPrefixes; - bestMatch = MatchFromList(host, path, list, out prefix); - - if (path != pathSlash && bestMatch == null) - bestMatch = MatchFromList(host, pathSlash, list, out prefix); - - if (bestMatch != null) - return bestMatch; - - return null; - } - - private HttpListener MatchFromList(string host, string path, List list, out ListenerPrefix prefix) - { - prefix = null; - if (list == null) - return null; - - HttpListener bestMatch = null; - int bestLength = -1; - - foreach (ListenerPrefix p in list) - { - string ppath = p.Path; - if (ppath.Length < bestLength) - continue; - - if (path.StartsWith(ppath)) - { - bestLength = ppath.Length; - bestMatch = p._listener; - prefix = p; - } - } - - return bestMatch; - } - - private void AddSpecial(List list, ListenerPrefix prefix) - { - if (list == null) - return; - - foreach (ListenerPrefix p in list) - { - if (p.Path == prefix.Path) - throw new Exception("net_listener_already"); - } - list.Add(prefix); - } - - private bool RemoveSpecial(List list, ListenerPrefix prefix) - { - if (list == null) - return false; - - int c = list.Count; - for (int i = 0; i < c; i++) - { - ListenerPrefix p = list[i]; - if (p.Path == prefix.Path) - { - list.RemoveAt(i); - return true; - } - } - return false; - } - - private void CheckIfRemove() - { - if (_prefixes.Count > 0) - return; - - List list = _unhandledPrefixes; - if (list != null && list.Count > 0) - return; - - list = _allPrefixes; - if (list != null && list.Count > 0) - return; - - HttpEndPointManager.RemoveEndPoint(this, _endpoint); - } - - public void Close() - { - _closed = true; - _socket.Close(); - lock (_unregisteredConnections) - { - // Clone the list because RemoveConnection can be called from Close - var connections = new List(_unregisteredConnections.Keys); - - foreach (HttpConnection c in connections) - c.Close(true); - _unregisteredConnections.Clear(); - } - } - - public void AddPrefix(ListenerPrefix prefix, HttpListener listener) - { - List current; - List future; - if (prefix.Host == "*") - { - do - { - current = _unhandledPrefixes; - future = current != null ? new List(current) : new List(); - prefix._listener = listener; - AddSpecial(future, prefix); - } while (Interlocked.CompareExchange(ref _unhandledPrefixes, future, current) != current); - return; - } - - if (prefix.Host == "+") - { - do - { - current = _allPrefixes; - future = current != null ? new List(current) : new List(); - prefix._listener = listener; - AddSpecial(future, prefix); - } while (Interlocked.CompareExchange(ref _allPrefixes, future, current) != current); - return; - } - - Dictionary prefs, p2; - do - { - prefs = _prefixes; - if (prefs.ContainsKey(prefix)) - { - throw new Exception("net_listener_already"); - } - p2 = new Dictionary(prefs); - p2[prefix] = listener; - } while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); - } - - public void RemovePrefix(ListenerPrefix prefix, HttpListener listener) - { - List current; - List future; - if (prefix.Host == "*") - { - do - { - current = _unhandledPrefixes; - future = current != null ? new List(current) : new List(); - if (!RemoveSpecial(future, prefix)) - break; // Prefix not found - } while (Interlocked.CompareExchange(ref _unhandledPrefixes, future, current) != current); - - CheckIfRemove(); - return; - } - - if (prefix.Host == "+") - { - do - { - current = _allPrefixes; - future = current != null ? new List(current) : new List(); - if (!RemoveSpecial(future, prefix)) - break; // Prefix not found - } while (Interlocked.CompareExchange(ref _allPrefixes, future, current) != current); - CheckIfRemove(); - return; - } - - Dictionary prefs, p2; - do - { - prefs = _prefixes; - if (!prefs.ContainsKey(prefix)) - break; - - p2 = new Dictionary(prefs); - p2.Remove(prefix); - } while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); - CheckIfRemove(); - } - } -} diff --git a/SocketHttpListener/Net/HttpEndPointManager.cs b/SocketHttpListener/Net/HttpEndPointManager.cs deleted file mode 100644 index 27b4244a6c..0000000000 --- a/SocketHttpListener/Net/HttpEndPointManager.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - internal sealed class HttpEndPointManager - { - private static Dictionary> s_ipEndPoints = new Dictionary>(); - - private HttpEndPointManager() - { - } - - public static void AddListener(ILogger logger, HttpListener listener) - { - var added = new List(); - try - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - foreach (string prefix in listener.Prefixes) - { - AddPrefixInternal(logger, prefix, listener); - added.Add(prefix); - } - } - } - catch - { - foreach (string prefix in added) - { - RemovePrefix(logger, prefix, listener); - } - throw; - } - } - - public static void AddPrefix(ILogger logger, string prefix, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - AddPrefixInternal(logger, prefix, listener); - } - } - - private static void AddPrefixInternal(ILogger logger, string p, HttpListener listener) - { - int start = p.IndexOf(':') + 3; - int colon = p.IndexOf(':', start); - if (colon != -1) - { - // root can't be -1 here, since we've already checked for ending '/' in ListenerPrefix. - int root = p.IndexOf('/', colon, p.Length - colon); - string portString = p.Substring(colon + 1, root - colon - 1); - - if (!int.TryParse(portString, out var port) || port <= 0 || port >= 65536) - { - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_port"); - } - } - - var lp = new ListenerPrefix(p); - if (lp.Host != "*" && lp.Host != "+" && Uri.CheckHostName(lp.Host) == UriHostNameType.Unknown) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_listener_host"); - - if (lp.Path.IndexOf('%') != -1) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_path"); - - if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_path"); - - // listens on all the interfaces if host name cannot be parsed by IPAddress. - HttpEndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure); - epl.AddPrefix(lp, listener); - } - - private static IPAddress GetIpAnyAddress(HttpListener listener) - { - return listener.EnableDualMode ? IPAddress.IPv6Any : IPAddress.Any; - } - - private static HttpEndPointListener GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure) - { - IPAddress addr; - if (host == "*" || host == "+") - { - addr = GetIpAnyAddress(listener); - } - else - { - const int NotSupportedErrorCode = 50; - try - { - addr = Dns.GetHostAddresses(host)[0]; - } - catch - { - // Throw same error code as windows, request is not supported. - throw new HttpListenerException(NotSupportedErrorCode, "net_listener_not_supported"); - } - - if (IPAddress.Any.Equals(addr)) - { - // Don't support listening to 0.0.0.0, match windows behavior. - throw new HttpListenerException(NotSupportedErrorCode, "net_listener_not_supported"); - } - } - - Dictionary p = null; - if (s_ipEndPoints.ContainsKey(addr)) - { - p = s_ipEndPoints[addr]; - } - else - { - p = new Dictionary(); - s_ipEndPoints[addr] = p; - } - - HttpEndPointListener epl = null; - if (p.ContainsKey(port)) - { - epl = p[port]; - } - else - { - try - { - epl = new HttpEndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.SocketFactory, listener.StreamHelper, listener.FileSystem, listener.EnvironmentInfo); - } - catch (SocketException ex) - { - throw new HttpListenerException(ex.ErrorCode, ex.Message); - } - p[port] = epl; - } - - return epl; - } - - public static void RemoveEndPoint(HttpEndPointListener epl, IPEndPoint ep) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - Dictionary p = null; - p = s_ipEndPoints[ep.Address]; - p.Remove(ep.Port); - if (p.Count == 0) - { - s_ipEndPoints.Remove(ep.Address); - } - epl.Close(); - } - } - - public static void RemoveListener(ILogger logger, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - foreach (string prefix in listener.Prefixes) - { - RemovePrefixInternal(logger, prefix, listener); - } - } - } - - public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - RemovePrefixInternal(logger, prefix, listener); - } - } - - private static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener) - { - var lp = new ListenerPrefix(prefix); - if (lp.Path.IndexOf('%') != -1) - return; - - if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) - return; - - HttpEndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure); - epl.RemovePrefix(lp, listener); - } - } -} diff --git a/SocketHttpListener/Net/HttpKnownHeaderNames.cs b/SocketHttpListener/Net/HttpKnownHeaderNames.cs deleted file mode 100644 index dc6f2ce415..0000000000 --- a/SocketHttpListener/Net/HttpKnownHeaderNames.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static partial class HttpKnownHeaderNames - { - // When adding a new constant, add it to HttpKnownHeaderNames.TryGetHeaderName.cs as well. - - public const string Accept = "Accept"; - public const string AcceptCharset = "Accept-Charset"; - public const string AcceptEncoding = "Accept-Encoding"; - public const string AcceptLanguage = "Accept-Language"; - public const string AcceptPatch = "Accept-Patch"; - public const string AcceptRanges = "Accept-Ranges"; - public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; - public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; - public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; - public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; - public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; - public const string AccessControlMaxAge = "Access-Control-Max-Age"; - public const string Age = "Age"; - public const string Allow = "Allow"; - public const string AltSvc = "Alt-Svc"; - public const string Authorization = "Authorization"; - public const string CacheControl = "Cache-Control"; - public const string Connection = "Connection"; - public const string ContentDisposition = "Content-Disposition"; - public const string ContentEncoding = "Content-Encoding"; - public const string ContentLanguage = "Content-Language"; - public const string ContentLength = "Content-Length"; - public const string ContentLocation = "Content-Location"; - public const string ContentMD5 = "Content-MD5"; - public const string ContentRange = "Content-Range"; - public const string ContentSecurityPolicy = "Content-Security-Policy"; - public const string ContentType = "Content-Type"; - public const string Cookie = "Cookie"; - public const string Cookie2 = "Cookie2"; - public const string Date = "Date"; - public const string ETag = "ETag"; - public const string Expect = "Expect"; - public const string Expires = "Expires"; - public const string From = "From"; - public const string Host = "Host"; - public const string IfMatch = "If-Match"; - public const string IfModifiedSince = "If-Modified-Since"; - public const string IfNoneMatch = "If-None-Match"; - public const string IfRange = "If-Range"; - public const string IfUnmodifiedSince = "If-Unmodified-Since"; - public const string KeepAlive = "Keep-Alive"; - public const string LastModified = "Last-Modified"; - public const string Link = "Link"; - public const string Location = "Location"; - public const string MaxForwards = "Max-Forwards"; - public const string Origin = "Origin"; - public const string P3P = "P3P"; - public const string Pragma = "Pragma"; - public const string ProxyAuthenticate = "Proxy-Authenticate"; - public const string ProxyAuthorization = "Proxy-Authorization"; - public const string ProxyConnection = "Proxy-Connection"; - public const string PublicKeyPins = "Public-Key-Pins"; - public const string Range = "Range"; - public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched. - public const string RetryAfter = "Retry-After"; - public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; - public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; - public const string SecWebSocketKey = "Sec-WebSocket-Key"; - public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; - public const string SecWebSocketVersion = "Sec-WebSocket-Version"; - public const string Server = "Server"; - public const string SetCookie = "Set-Cookie"; - public const string SetCookie2 = "Set-Cookie2"; - public const string StrictTransportSecurity = "Strict-Transport-Security"; - public const string TE = "TE"; - public const string TSV = "TSV"; - public const string Trailer = "Trailer"; - public const string TransferEncoding = "Transfer-Encoding"; - public const string Upgrade = "Upgrade"; - public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; - public const string UserAgent = "User-Agent"; - public const string Vary = "Vary"; - public const string Via = "Via"; - public const string WWWAuthenticate = "WWW-Authenticate"; - public const string Warning = "Warning"; - public const string XAspNetVersion = "X-AspNet-Version"; - public const string XContentDuration = "X-Content-Duration"; - public const string XContentTypeOptions = "X-Content-Type-Options"; - public const string XFrameOptions = "X-Frame-Options"; - public const string XMSEdgeRef = "X-MSEdge-Ref"; - public const string XPoweredBy = "X-Powered-By"; - public const string XRequestID = "X-Request-ID"; - public const string XUACompatible = "X-UA-Compatible"; - } -} diff --git a/SocketHttpListener/Net/HttpListener.cs b/SocketHttpListener/Net/HttpListener.cs deleted file mode 100644 index f17036a21a..0000000000 --- a/SocketHttpListener/Net/HttpListener.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - public sealed class HttpListener : IDisposable - { - internal ICryptoProvider CryptoProvider { get; private set; } - internal ISocketFactory SocketFactory { get; private set; } - internal IFileSystem FileSystem { get; private set; } - internal IStreamHelper StreamHelper { get; private set; } - internal IEnvironmentInfo EnvironmentInfo { get; private set; } - - public bool EnableDualMode { get; set; } - - private AuthenticationSchemes auth_schemes; - private HttpListenerPrefixCollection prefixes; - private AuthenticationSchemeSelector auth_selector; - private string realm; - private bool unsafe_ntlm_auth; - private bool listening; - private bool disposed; - - private Dictionary registry; - private Dictionary connections; - private ILogger _logger; - private X509Certificate _certificate; - - public Action OnContext { get; set; } - - public HttpListener( - ILogger logger, - ICryptoProvider cryptoProvider, - ISocketFactory socketFactory, - IStreamHelper streamHelper, - IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) - { - _logger = logger; - CryptoProvider = cryptoProvider; - SocketFactory = socketFactory; - StreamHelper = streamHelper; - FileSystem = fileSystem; - EnvironmentInfo = environmentInfo; - - prefixes = new HttpListenerPrefixCollection(logger, this); - registry = new Dictionary(); - connections = new Dictionary(); - auth_schemes = AuthenticationSchemes.Anonymous; - } - - public HttpListener( - ILogger logger, - X509Certificate certificate, - ICryptoProvider cryptoProvider, - ISocketFactory socketFactory, - IStreamHelper streamHelper, - IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) - : this(logger, cryptoProvider, socketFactory, streamHelper, fileSystem, environmentInfo) - { - _certificate = certificate; - } - - public void LoadCert(X509Certificate cert) - { - _certificate = cert; - } - - // TODO: Digest, NTLM and Negotiate require ControlPrincipal - public AuthenticationSchemes AuthenticationSchemes - { - get => auth_schemes; - set - { - CheckDisposed(); - auth_schemes = value; - } - } - - public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate - { - get => auth_selector; - set - { - CheckDisposed(); - auth_selector = value; - } - } - - public bool IsListening => listening; - - public static bool IsSupported => true; - - public HttpListenerPrefixCollection Prefixes - { - get - { - CheckDisposed(); - return prefixes; - } - } - - // TODO: use this - public string Realm - { - get => realm; - set - { - CheckDisposed(); - realm = value; - } - } - - public bool UnsafeConnectionNtlmAuthentication - { - get => unsafe_ntlm_auth; - set - { - CheckDisposed(); - unsafe_ntlm_auth = value; - } - } - - //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback) - //{ - // lock (registry) - // { - // if (tlsProvider == null) - // tlsProvider = MonoTlsProviderFactory.GetProviderInternal(); - // if (tlsSettings == null) - // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings(); - // if (tlsSettings.RemoteCertificateValidationCallback == null) - // tlsSettings.RemoteCertificateValidationCallback = callback; - // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings); - // } - //} - - internal X509Certificate Certificate => _certificate; - - public void Abort() - { - if (disposed) - return; - - if (!listening) - { - return; - } - - Close(true); - } - - public void Close() - { - if (disposed) - return; - - if (!listening) - { - disposed = true; - return; - } - - Close(true); - disposed = true; - } - - void Close(bool force) - { - CheckDisposed(); - HttpEndPointManager.RemoveListener(_logger, this); - Cleanup(force); - } - - void Cleanup(bool close_existing) - { - lock (registry) - { - if (close_existing) - { - // Need to copy this since closing will call UnregisterContext - ICollection keys = registry.Keys; - var all = new HttpListenerContext[keys.Count]; - keys.CopyTo(all, 0); - registry.Clear(); - for (int i = all.Length - 1; i >= 0; i--) - all[i].Connection.Close(true); - } - - lock (connections) - { - ICollection keys = connections.Keys; - var conns = new HttpConnection[keys.Count]; - keys.CopyTo(conns, 0); - connections.Clear(); - for (int i = conns.Length - 1; i >= 0; i--) - conns[i].Close(true); - } - } - } - - internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context) - { - if (AuthenticationSchemeSelectorDelegate != null) - return AuthenticationSchemeSelectorDelegate(context.Request); - else - return auth_schemes; - } - - public void Start() - { - CheckDisposed(); - if (listening) - return; - - HttpEndPointManager.AddListener(_logger, this); - listening = true; - } - - public void Stop() - { - CheckDisposed(); - listening = false; - Close(false); - } - - void IDisposable.Dispose() - { - if (disposed) - return; - - Close(true); //TODO: Should we force here or not? - disposed = true; - } - - internal void CheckDisposed() - { - if (disposed) - throw new ObjectDisposedException(GetType().Name); - } - - internal void RegisterContext(HttpListenerContext context) - { - if (OnContext != null && IsListening) - { - OnContext(context); - } - - lock (registry) - registry[context] = context; - } - - internal void UnregisterContext(HttpListenerContext context) - { - lock (registry) - registry.Remove(context); - } - - internal void AddConnection(HttpConnection cnc) - { - lock (connections) - { - connections[cnc] = cnc; - } - } - - internal void RemoveConnection(HttpConnection cnc) - { - lock (connections) - { - connections.Remove(cnc); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener/Net/HttpListenerBasicIdentity.cs deleted file mode 100644 index 5f6ec44b96..0000000000 --- a/SocketHttpListener/Net/HttpListenerBasicIdentity.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Security.Principal; - -namespace SocketHttpListener.Net -{ - public class HttpListenerBasicIdentity : GenericIdentity - { - string password; - - public HttpListenerBasicIdentity(string username, string password) - : base(username, "Basic") - { - this.password = password; - } - - public virtual string Password => password; - } - - public class GenericIdentity : IIdentity - { - private string m_name; - private string m_type; - - public GenericIdentity(string name) - { - if (name == null) - throw new System.ArgumentNullException(nameof(name)); - - m_name = name; - m_type = ""; - } - - public GenericIdentity(string name, string type) - { - if (name == null) - throw new System.ArgumentNullException(nameof(name)); - if (type == null) - throw new System.ArgumentNullException(nameof(type)); - - m_name = name; - m_type = type; - } - - public virtual string Name => m_name; - - public virtual string AuthenticationType => m_type; - - public virtual bool IsAuthenticated => !m_name.Equals(""); - } -} diff --git a/SocketHttpListener/Net/HttpListenerContext.Managed.cs b/SocketHttpListener/Net/HttpListenerContext.Managed.cs deleted file mode 100644 index 79742bf37f..0000000000 --- a/SocketHttpListener/Net/HttpListenerContext.Managed.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.ComponentModel; -using System.Security.Principal; -using System.Text; -using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerContext - { - private HttpConnection _connection; - - internal HttpListenerContext(HttpConnection connection) - { - _connection = connection; - _response = new HttpListenerResponse(this); - Request = new HttpListenerRequest(this); - ErrorStatus = 400; - } - - internal int ErrorStatus { get; set; } - - internal string ErrorMessage { get; set; } - - internal bool HaveError => ErrorMessage != null; - - internal HttpConnection Connection => _connection; - - internal void ParseAuthentication(System.Net.AuthenticationSchemes expectedSchemes) - { - if (expectedSchemes == System.Net.AuthenticationSchemes.Anonymous) - return; - - string header = Request.Headers["Authorization"]; - if (string.IsNullOrEmpty(header)) - return; - - if (IsBasicHeader(header)) - { - _user = ParseBasicAuthentication(header.Substring(AuthenticationTypes.Basic.Length + 1)); - } - } - - internal IPrincipal ParseBasicAuthentication(string authData) => - TryParseBasicAuth(authData, out HttpStatusCode errorCode, out string username, out string password) ? - new GenericPrincipal(new HttpListenerBasicIdentity(username, password), Array.Empty()) : - null; - - internal static bool IsBasicHeader(string header) => - header.Length >= 6 && - header[5] == ' ' && - string.Compare(header, 0, AuthenticationTypes.Basic, 0, 5, StringComparison.OrdinalIgnoreCase) == 0; - - internal static bool TryParseBasicAuth(string headerValue, out HttpStatusCode errorCode, out string username, out string password) - { - errorCode = HttpStatusCode.OK; - username = password = null; - try - { - if (string.IsNullOrWhiteSpace(headerValue)) - { - return false; - } - - string authString = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)); - int colonPos = authString.IndexOf(':'); - if (colonPos < 0) - { - // username must be at least 1 char - errorCode = HttpStatusCode.BadRequest; - return false; - } - - username = authString.Substring(0, colonPos); - password = authString.Substring(colonPos + 1); - return true; - } - catch - { - errorCode = HttpStatusCode.InternalServerError; - return false; - } - } - - public Task AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval) - { - return HttpWebSocket.AcceptWebSocketAsyncCore(this, subProtocol, receiveBufferSize, keepAliveInterval); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public Task AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval, ArraySegment internalBuffer) - { - WebSocketValidate.ValidateArraySegment(internalBuffer, nameof(internalBuffer)); - HttpWebSocket.ValidateOptions(subProtocol, receiveBufferSize, HttpWebSocket.MinSendBufferSize, keepAliveInterval); - return HttpWebSocket.AcceptWebSocketAsyncCore(this, subProtocol, receiveBufferSize, keepAliveInterval, internalBuffer); - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerContext.cs b/SocketHttpListener/Net/HttpListenerContext.cs deleted file mode 100644 index d84e2d1aa0..0000000000 --- a/SocketHttpListener/Net/HttpListenerContext.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net; -using System.Security.Principal; -using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerContext - { - private HttpListenerResponse _response; - private IPrincipal _user; - - public HttpListenerRequest Request { get; } - - public IPrincipal User => _user; - - // This can be used to cache the results of HttpListener.AuthenticationSchemeSelectorDelegate. - internal AuthenticationSchemes AuthenticationSchemes { get; set; } - - public HttpListenerResponse Response => _response; - - public Task AcceptWebSocketAsync(string subProtocol) - { - return AcceptWebSocketAsync(subProtocol, HttpWebSocket.DefaultReceiveBufferSize, WebSocket.DefaultKeepAliveInterval); - } - - public Task AcceptWebSocketAsync(string subProtocol, TimeSpan keepAliveInterval) - { - return AcceptWebSocketAsync(subProtocol, HttpWebSocket.DefaultReceiveBufferSize, keepAliveInterval); - } - } - - public class GenericPrincipal : IPrincipal - { - private IIdentity m_identity; - private string[] m_roles; - - public GenericPrincipal(IIdentity identity, string[] roles) - { - if (identity == null) - throw new ArgumentNullException(nameof(identity)); - - m_identity = identity; - if (roles != null) - { - m_roles = new string[roles.Length]; - for (int i = 0; i < roles.Length; ++i) - { - m_roles[i] = roles[i]; - } - } - else - { - m_roles = null; - } - } - - public virtual IIdentity Identity => m_identity; - - public virtual bool IsInRole(string role) - { - if (role == null || m_roles == null) - return false; - - for (int i = 0; i < m_roles.Length; ++i) - { - if (m_roles[i] != null && string.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0) - return true; - } - return false; - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs deleted file mode 100644 index 400a1adb61..0000000000 --- a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable - { - private List _prefixes = new List(); - private HttpListener _listener; - - private ILogger _logger; - - internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) - { - _logger = logger; - _listener = listener; - } - - public int Count => _prefixes.Count; - - public bool IsReadOnly => false; - - public bool IsSynchronized => false; - - public void Add(string uriPrefix) - { - _listener.CheckDisposed(); - //ListenerPrefix.CheckUri(uriPrefix); - if (_prefixes.Contains(uriPrefix)) - { - return; - } - - _prefixes.Add(uriPrefix); - if (_listener.IsListening) - { - HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); - } - } - - public void AddRange(IEnumerable uriPrefixes) - { - _listener.CheckDisposed(); - - foreach (var uriPrefix in uriPrefixes) - { - if (_prefixes.Contains(uriPrefix)) - { - continue; - } - - _prefixes.Add(uriPrefix); - if (_listener.IsListening) - { - HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); - } - } - } - - public void Clear() - { - _listener.CheckDisposed(); - _prefixes.Clear(); - if (_listener.IsListening) - { - HttpEndPointManager.RemoveListener(_logger, _listener); - } - } - - public bool Contains(string uriPrefix) - { - _listener.CheckDisposed(); - return _prefixes.Contains(uriPrefix); - } - - public void CopyTo(string[] array, int offset) - { - _listener.CheckDisposed(); - _prefixes.CopyTo(array, offset); - } - - public void CopyTo(Array array, int offset) - { - _listener.CheckDisposed(); - ((ICollection)_prefixes).CopyTo(array, offset); - } - - public IEnumerator GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - public bool Remove(string uriPrefix) - { - _listener.CheckDisposed(); - if (uriPrefix == null) - { - throw new ArgumentNullException(nameof(uriPrefix)); - } - - bool result = _prefixes.Remove(uriPrefix); - if (result && _listener.IsListening) - { - HttpEndPointManager.RemovePrefix(_logger, uriPrefix, _listener); - } - - return result; - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequest.Managed.cs b/SocketHttpListener/Net/HttpListenerRequest.Managed.cs deleted file mode 100644 index 3f9e32f089..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequest.Managed.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.IO; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerRequest - { - private long _contentLength; - private bool _clSet; - private WebHeaderCollection _headers; - private string _method; - private Stream _inputStream; - private HttpListenerContext _context; - private bool _isChunked; - - private static byte[] s_100continue = Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); - - internal HttpListenerRequest(HttpListenerContext context) - { - _context = context; - _headers = new WebHeaderCollection(); - _version = HttpVersion.Version10; - } - - private static readonly char[] s_separators = new char[] { ' ' }; - - internal void SetRequestLine(string req) - { - string[] parts = req.Split(s_separators, 3); - if (parts.Length != 3) - { - _context.ErrorMessage = "Invalid request line (parts)."; - return; - } - - _method = parts[0]; - foreach (char c in _method) - { - int ic = (int)c; - - if ((ic >= 'A' && ic <= 'Z') || - (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && - c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && - c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && - c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) - continue; - - _context.ErrorMessage = "(Invalid verb)"; - return; - } - - _rawUrl = parts[1]; - if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - - try - { - _version = new Version(parts[2].Substring(5)); - } - catch - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - - if (_version.Major < 1) - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - if (_version.Major > 1) - { - _context.ErrorStatus = (int)HttpStatusCode.HttpVersionNotSupported; - _context.ErrorMessage = HttpStatusDescription.Get(HttpStatusCode.HttpVersionNotSupported); - return; - } - } - - private static bool MaybeUri(string s) - { - int p = s.IndexOf(':'); - if (p == -1) - return false; - - if (p >= 10) - return false; - - return IsPredefinedScheme(s.Substring(0, p)); - } - - private static bool IsPredefinedScheme(string scheme) - { - if (scheme == null || scheme.Length < 3) - return false; - - char c = scheme[0]; - if (c == 'h') - return (scheme == UriScheme.Http || scheme == UriScheme.Https); - if (c == 'f') - return (scheme == UriScheme.File || scheme == UriScheme.Ftp); - - if (c == 'n') - { - c = scheme[1]; - if (c == 'e') - return (scheme == UriScheme.News || scheme == UriScheme.NetPipe || scheme == UriScheme.NetTcp); - if (scheme == UriScheme.Nntp) - return true; - return false; - } - if ((c == 'g' && scheme == UriScheme.Gopher) || (c == 'm' && scheme == UriScheme.Mailto)) - return true; - - return false; - } - - internal void FinishInitialization() - { - string host = UserHostName; - if (_version > HttpVersion.Version10 && (host == null || host.Length == 0)) - { - _context.ErrorMessage = "Invalid host name"; - return; - } - - string path; - Uri raw_uri = null; - if (MaybeUri(_rawUrl.ToLowerInvariant()) && Uri.TryCreate(_rawUrl, UriKind.Absolute, out raw_uri)) - path = raw_uri.PathAndQuery; - else - path = _rawUrl; - - if ((host == null || host.Length == 0)) - host = UserHostAddress; - - if (raw_uri != null) - host = raw_uri.Host; - - int colon = host.IndexOf(']') == -1 ? host.IndexOf(':') : host.LastIndexOf(':'); - if (colon >= 0) - host = host.Substring(0, colon); - - string base_uri = string.Format("{0}://{1}:{2}", RequestScheme, host, LocalEndPoint.Port); - - if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out _requestUri)) - { - _context.ErrorMessage = System.Net.WebUtility.HtmlEncode("Invalid url: " + base_uri + path); - return; - } - - _requestUri = HttpListenerRequestUriBuilder.GetRequestUri(_rawUrl, _requestUri.Scheme, - _requestUri.Authority, _requestUri.LocalPath, _requestUri.Query); - - if (_version >= HttpVersion.Version11) - { - string t_encoding = Headers[HttpKnownHeaderNames.TransferEncoding]; - _isChunked = (t_encoding != null && string.Equals(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase)); - // 'identity' is not valid! - if (t_encoding != null && !_isChunked) - { - _context.Connection.SendError(null, 501); - return; - } - } - - if (!_isChunked && !_clSet) - { - if (string.Equals(_method, "POST", StringComparison.OrdinalIgnoreCase) || - string.Equals(_method, "PUT", StringComparison.OrdinalIgnoreCase)) - { - _context.Connection.SendError(null, 411); - return; - } - } - - if (string.Compare(Headers[HttpKnownHeaderNames.Expect], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) - { - HttpResponseStream output = _context.Connection.GetResponseStream(); - output.InternalWrite(s_100continue, 0, s_100continue.Length); - } - } - - internal static string Unquote(string str) - { - int start = str.IndexOf('\"'); - int end = str.LastIndexOf('\"'); - if (start >= 0 && end >= 0) - str = str.Substring(start + 1, end - 1); - return str.Trim(); - } - - internal void AddHeader(string header) - { - int colon = header.IndexOf(':'); - if (colon == -1 || colon == 0) - { - _context.ErrorMessage = HttpStatusDescription.Get(400); - _context.ErrorStatus = 400; - return; - } - - string name = header.Substring(0, colon).Trim(); - string val = header.Substring(colon + 1).Trim(); - if (name.Equals("content-length", StringComparison.OrdinalIgnoreCase)) - { - // To match Windows behavior: - // Content lengths >= 0 and <= long.MaxValue are accepted as is. - // Content lengths > long.MaxValue and <= ulong.MaxValue are treated as 0. - // Content lengths < 0 cause the requests to fail. - // Other input is a failure, too. - long parsedContentLength = - ulong.TryParse(val, out ulong parsedUlongContentLength) ? (parsedUlongContentLength <= long.MaxValue ? (long)parsedUlongContentLength : 0) : - long.Parse(val); - if (parsedContentLength < 0 || (_clSet && parsedContentLength != _contentLength)) - { - _context.ErrorMessage = "Invalid Content-Length."; - } - else - { - _contentLength = parsedContentLength; - _clSet = true; - } - } - else if (name.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase)) - { - if (Headers[HttpKnownHeaderNames.TransferEncoding] != null) - { - _context.ErrorStatus = (int)HttpStatusCode.NotImplemented; - _context.ErrorMessage = HttpStatusDescription.Get(HttpStatusCode.NotImplemented); - } - } - - if (_context.ErrorMessage == null) - { - _headers.Set(name, val); - } - } - - // returns true is the stream could be reused. - internal bool FlushInput() - { - if (!HasEntityBody) - return true; - - int length = 2048; - if (_contentLength > 0) - length = (int)Math.Min(_contentLength, (long)length); - - byte[] bytes = new byte[length]; - while (true) - { - try - { - IAsyncResult ares = InputStream.BeginRead(bytes, 0, length, null, null); - if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne(1000)) - return false; - if (InputStream.EndRead(ares) <= 0) - return true; - } - catch (ObjectDisposedException) - { - _inputStream = null; - return true; - } - catch - { - return false; - } - } - } - - public long ContentLength64 - { - get - { - if (_isChunked) - _contentLength = -1; - - return _contentLength; - } - } - - public bool HasEntityBody => (_contentLength > 0 || _isChunked); - - public QueryParamCollection Headers => _headers; - - public string HttpMethod => _method; - - public Stream InputStream - { - get - { - if (_inputStream == null) - { - if (_isChunked || _contentLength > 0) - _inputStream = _context.Connection.GetRequestStream(_isChunked, _contentLength); - else - _inputStream = Stream.Null; - } - - return _inputStream; - } - } - - public bool IsAuthenticated => false; - - public bool IsSecureConnection => _context.Connection.IsSecure; - - public System.Net.IPEndPoint LocalEndPoint => _context.Connection.LocalEndPoint; - - public System.Net.IPEndPoint RemoteEndPoint => _context.Connection.RemoteEndPoint; - - public Guid RequestTraceIdentifier { get; } = Guid.NewGuid(); - - public string ServiceName => null; - - private Uri RequestUri => _requestUri; - private bool SupportsWebSockets => true; - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs deleted file mode 100644 index 667d58ea7b..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequest.cs +++ /dev/null @@ -1,537 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerRequest - { - private CookieCollection _cookies; - private bool? _keepAlive; - private string _rawUrl; - private Uri _requestUri; - private Version _version; - - public string[] AcceptTypes => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.Accept]); - - public string[] UserLanguages => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.AcceptLanguage]); - - private static CookieCollection ParseCookies(Uri uri, string setCookieHeader) - { - var cookies = new CookieCollection(); - return cookies; - } - - public CookieCollection Cookies - { - get - { - if (_cookies == null) - { - string cookieString = Headers[HttpKnownHeaderNames.Cookie]; - if (!string.IsNullOrEmpty(cookieString)) - { - _cookies = ParseCookies(RequestUri, cookieString); - } - if (_cookies == null) - { - _cookies = new CookieCollection(); - } - } - return _cookies; - } - } - - public Encoding ContentEncoding - { - get - { - if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) - { - string postDataCharset = Headers["x-up-devcap-post-charset"]; - if (postDataCharset != null && postDataCharset.Length > 0) - { - try - { - return Encoding.GetEncoding(postDataCharset); - } - catch (ArgumentException) - { - } - } - } - if (HasEntityBody) - { - if (ContentType != null) - { - string charSet = Helpers.GetCharSetValueFromHeader(ContentType); - if (charSet != null) - { - try - { - return Encoding.GetEncoding(charSet); - } - catch (ArgumentException) - { - } - } - } - } - return Encoding.UTF8; - } - } - - public string ContentType => Headers[HttpKnownHeaderNames.ContentType]; - - public bool IsLocal => LocalEndPoint.Address.Equals(RemoteEndPoint.Address); - - public bool IsWebSocketRequest - { - get - { - if (!SupportsWebSockets) - { - return false; - } - - bool foundConnectionUpgradeHeader = false; - if (string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Connection]) || string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Upgrade])) - { - return false; - } - - foreach (string connection in Headers.GetValues(HttpKnownHeaderNames.Connection)) - { - if (string.Equals(connection, HttpKnownHeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase)) - { - foundConnectionUpgradeHeader = true; - break; - } - } - - if (!foundConnectionUpgradeHeader) - { - return false; - } - - foreach (string upgrade in Headers.GetValues(HttpKnownHeaderNames.Upgrade)) - { - if (string.Equals(upgrade, HttpWebSocket.WebSocketUpgradeToken, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - } - - public bool KeepAlive - { - get - { - if (!_keepAlive.HasValue) - { - string header = Headers[HttpKnownHeaderNames.ProxyConnection]; - if (string.IsNullOrEmpty(header)) - { - header = Headers[HttpKnownHeaderNames.Connection]; - } - if (string.IsNullOrEmpty(header)) - { - if (ProtocolVersion >= HttpVersion.Version11) - { - _keepAlive = true; - } - else - { - header = Headers[HttpKnownHeaderNames.KeepAlive]; - _keepAlive = !string.IsNullOrEmpty(header); - } - } - else - { - header = header.ToLowerInvariant(); - _keepAlive = - header.IndexOf("close", StringComparison.OrdinalIgnoreCase) < 0 || - header.IndexOf("keep-alive", StringComparison.OrdinalIgnoreCase) >= 0; - } - } - - return _keepAlive.Value; - } - } - - public QueryParamCollection QueryString - { - get - { - var queryString = new QueryParamCollection(); - Helpers.FillFromString(queryString, Url.Query, true, ContentEncoding); - return queryString; - } - } - - public string RawUrl => _rawUrl; - - private string RequestScheme => IsSecureConnection ? UriScheme.Https : UriScheme.Http; - - public string UserAgent => Headers[HttpKnownHeaderNames.UserAgent]; - - public string UserHostAddress => LocalEndPoint.ToString(); - - public string UserHostName => Headers[HttpKnownHeaderNames.Host]; - - public Uri UrlReferrer - { - get - { - string referrer = Headers[HttpKnownHeaderNames.Referer]; - if (referrer == null) - { - return null; - } - - bool success = Uri.TryCreate(referrer, UriKind.RelativeOrAbsolute, out var urlReferrer); - return success ? urlReferrer : null; - } - } - - public Uri Url => RequestUri; - - public Version ProtocolVersion => _version; - - private static class Helpers - { - // - // Get attribute off header value - // - internal static string GetCharSetValueFromHeader(string headerValue) - { - const string AttrName = "charset"; - - if (headerValue == null) - return null; - - int l = headerValue.Length; - int k = AttrName.Length; - - // find properly separated attribute name - int i = 1; // start searching from 1 - - while (i < l) - { - i = CultureInfo.InvariantCulture.CompareInfo.IndexOf(headerValue, AttrName, i, CompareOptions.IgnoreCase); - if (i < 0) - break; - if (i + k >= l) - break; - - char chPrev = headerValue[i - 1]; - char chNext = headerValue[i + k]; - if ((chPrev == ';' || chPrev == ',' || char.IsWhiteSpace(chPrev)) && (chNext == '=' || char.IsWhiteSpace(chNext))) - break; - - i += k; - } - - if (i < 0 || i >= l) - return null; - - // skip to '=' and the following whitespace - i += k; - while (i < l && char.IsWhiteSpace(headerValue[i])) - i++; - if (i >= l || headerValue[i] != '=') - return null; - i++; - while (i < l && char.IsWhiteSpace(headerValue[i])) - i++; - if (i >= l) - return null; - - // parse the value - string attrValue = null; - - int j; - - if (i < l && headerValue[i] == '"') - { - if (i == l - 1) - return null; - j = headerValue.IndexOf('"', i + 1); - if (j < 0 || j == i + 1) - return null; - - attrValue = headerValue.Substring(i + 1, j - i - 1).Trim(); - } - else - { - for (j = i; j < l; j++) - { - if (headerValue[j] == ';') - break; - } - - if (j == i) - return null; - - attrValue = headerValue.Substring(i, j - i).Trim(); - } - - return attrValue; - } - - internal static string[] ParseMultivalueHeader(string s) - { - if (s == null) - return null; - - int l = s.Length; - - // collect comma-separated values into list - - var values = new List(); - int i = 0; - - while (i < l) - { - // find next , - int ci = s.IndexOf(',', i); - if (ci < 0) - ci = l; - - // append corresponding server value - values.Add(s.Substring(i, ci - i)); - - // move to next - i = ci + 1; - - // skip leading space - if (i < l && s[i] == ' ') - i++; - } - - // return list as array of strings - - int n = values.Count; - string[] strings; - - // if n is 0 that means s was empty string - - if (n == 0) - { - strings = new string[1]; - strings[0] = string.Empty; - } - else - { - strings = new string[n]; - values.CopyTo(0, strings, 0, n); - } - return strings; - } - - - private static string UrlDecodeStringFromStringInternal(string s, Encoding e) - { - int count = s.Length; - var helper = new UrlDecoder(count, e); - - // go through the string's chars collapsing %XX and %uXXXX and - // appending each char as char, with exception of %XX constructs - // that are appended as bytes - - for (int pos = 0; pos < count; pos++) - { - char ch = s[pos]; - - if (ch == '+') - { - ch = ' '; - } - else if (ch == '%' && pos < count - 2) - { - if (s[pos + 1] == 'u' && pos < count - 5) - { - int h1 = HexToInt(s[pos + 2]); - int h2 = HexToInt(s[pos + 3]); - int h3 = HexToInt(s[pos + 4]); - int h4 = HexToInt(s[pos + 5]); - - if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) - { // valid 4 hex chars - ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4); - pos += 5; - - // only add as char - helper.AddChar(ch); - continue; - } - } - else - { - int h1 = HexToInt(s[pos + 1]); - int h2 = HexToInt(s[pos + 2]); - - if (h1 >= 0 && h2 >= 0) - { // valid 2 hex chars - byte b = (byte)((h1 << 4) | h2); - pos += 2; - - // don't add as char - helper.AddByte(b); - continue; - } - } - } - - if ((ch & 0xFF80) == 0) - helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode - else - helper.AddChar(ch); - } - - return helper.GetString(); - } - - private static int HexToInt(char h) - { - return (h >= '0' && h <= '9') ? h - '0' : - (h >= 'a' && h <= 'f') ? h - 'a' + 10 : - (h >= 'A' && h <= 'F') ? h - 'A' + 10 : - -1; - } - - private class UrlDecoder - { - private int _bufferSize; - - // Accumulate characters in a special array - private int _numChars; - private char[] _charBuffer; - - // Accumulate bytes for decoding into characters in a special array - private int _numBytes; - private byte[] _byteBuffer; - - // Encoding to convert chars to bytes - private Encoding _encoding; - - private void FlushBytes() - { - if (_numBytes > 0) - { - _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); - _numBytes = 0; - } - } - - internal UrlDecoder(int bufferSize, Encoding encoding) - { - _bufferSize = bufferSize; - _encoding = encoding; - - _charBuffer = new char[bufferSize]; - // byte buffer created on demand - } - - internal void AddChar(char ch) - { - if (_numBytes > 0) - FlushBytes(); - - _charBuffer[_numChars++] = ch; - } - - internal void AddByte(byte b) - { - { - if (_byteBuffer == null) - _byteBuffer = new byte[_bufferSize]; - - _byteBuffer[_numBytes++] = b; - } - } - - internal string GetString() - { - if (_numBytes > 0) - FlushBytes(); - - if (_numChars > 0) - return new string(_charBuffer, 0, _numChars); - else - return string.Empty; - } - } - - - internal static void FillFromString(QueryParamCollection nvc, string s, bool urlencoded, Encoding encoding) - { - int l = (s != null) ? s.Length : 0; - int i = (s.Length > 0 && s[0] == '?') ? 1 : 0; - - while (i < l) - { - // find next & while noting first = on the way (and if there are more) - - int si = i; - int ti = -1; - - while (i < l) - { - char ch = s[i]; - - if (ch == '=') - { - if (ti < 0) - ti = i; - } - else if (ch == '&') - { - break; - } - - i++; - } - - // extract the name / value pair - - string name = null; - string value = null; - - if (ti >= 0) - { - name = s.Substring(si, ti - si); - value = s.Substring(ti + 1, i - ti - 1); - } - else - { - value = s.Substring(si, i - si); - } - - // add name / value pair to the collection - - if (urlencoded) - nvc.Add( - name == null ? null : UrlDecodeStringFromStringInternal(name, encoding), - UrlDecodeStringFromStringInternal(value, encoding)); - else - nvc.Add(name, value); - - // trailing '&' - - if (i == l - 1 && s[i] == '&') - nvc.Add(null, ""); - - i++; - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs b/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs deleted file mode 100644 index 7b4b619e68..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Text; - -namespace SocketHttpListener.Net -{ - // We don't use the cooked URL because http.sys unescapes all percent-encoded values. However, - // we also can't just use the raw Uri, since http.sys supports not only Utf-8, but also ANSI/DBCS and - // Unicode code points. System.Uri only supports Utf-8. - // The purpose of this class is to convert all ANSI, DBCS, and Unicode code points into percent encoded - // Utf-8 characters. - internal sealed class HttpListenerRequestUriBuilder - { - private static readonly Encoding s_utf8Encoding = new UTF8Encoding(false, true); - private static readonly Encoding s_ansiEncoding = Encoding.GetEncoding(0, new EncoderExceptionFallback(), new DecoderExceptionFallback()); - - private readonly string _rawUri; - private readonly string _cookedUriScheme; - private readonly string _cookedUriHost; - private readonly string _cookedUriPath; - private readonly string _cookedUriQuery; - - // This field is used to build the final request Uri string from the Uri parts passed to the ctor. - private StringBuilder _requestUriString; - - // The raw path is parsed by looping through all characters from left to right. 'rawOctets' - // is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%2F/ - // rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x2F } when - // we reach the final '/'. I.e. after a sequence of percent encoded octets ends, we use rawOctets as - // input to the encoding and percent encode the resulting string into UTF-8 octets. - // - // When parsing ANSI (Latin 1) encoded path '/pa%C4th/', %C4 will be added to rawOctets and when - // we reach 't', the content of rawOctets { 0xC4 } will be fed into the ANSI encoding. The resulting - // string 'Ä' will be percent encoded into UTF-8 octets and appended to requestUriString. The final - // path will be '/pa%C3%84th/', where '%C3%84' is the UTF-8 percent encoded character 'Ä'. - private List _rawOctets; - private string _rawPath; - - // Holds the final request Uri. - private Uri _requestUri; - - private HttpListenerRequestUriBuilder(string rawUri, string cookedUriScheme, string cookedUriHost, - string cookedUriPath, string cookedUriQuery) - { - _rawUri = rawUri; - _cookedUriScheme = cookedUriScheme; - _cookedUriHost = cookedUriHost; - _cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath); - _cookedUriQuery = cookedUriQuery ?? string.Empty; - } - - public static Uri GetRequestUri(string rawUri, string cookedUriScheme, string cookedUriHost, - string cookedUriPath, string cookedUriQuery) - { - var builder = new HttpListenerRequestUriBuilder(rawUri, - cookedUriScheme, cookedUriHost, cookedUriPath, cookedUriQuery); - - return builder.Build(); - } - - private Uri Build() - { - BuildRequestUriUsingRawPath(); - - if (_requestUri == null) - { - BuildRequestUriUsingCookedPath(); - } - - return _requestUri; - } - - private void BuildRequestUriUsingCookedPath() - { - bool isValid = Uri.TryCreate(_cookedUriScheme + Uri.SchemeDelimiter + _cookedUriHost + _cookedUriPath + - _cookedUriQuery, UriKind.Absolute, out _requestUri); - - // Creating a Uri from the cooked Uri should really always work: If not, we log at least. - if (!isValid) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_create_uri, _cookedUriScheme, _cookedUriHost, _cookedUriPath, _cookedUriQuery)); - } - } - - private void BuildRequestUriUsingRawPath() - { - bool isValid = false; - - // Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri. - _rawPath = GetPath(_rawUri); - - // Try to check the raw path using first the primary encoding (according to http.sys settings); - // if it fails try the secondary encoding. - ParsingResult result = BuildRequestUriUsingRawPath(GetEncoding(EncodingType.Primary)); - if (result == ParsingResult.EncodingError) - { - Encoding secondaryEncoding = GetEncoding(EncodingType.Secondary); - result = BuildRequestUriUsingRawPath(secondaryEncoding); - } - isValid = (result == ParsingResult.Success) ? true : false; - - // Log that we weren't able to create a Uri from the raw string. - if (!isValid) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_create_uri, _cookedUriScheme, _cookedUriHost, _rawPath, _cookedUriQuery)); - } - } - - private static Encoding GetEncoding(EncodingType type) - { - Debug.Assert((type == EncodingType.Primary) || (type == EncodingType.Secondary), - "Unknown 'EncodingType' value: " + type.ToString()); - - if (type == EncodingType.Secondary) - { - return s_ansiEncoding; - } - else - { - return s_utf8Encoding; - } - } - - private ParsingResult BuildRequestUriUsingRawPath(Encoding encoding) - { - Debug.Assert(encoding != null, "'encoding' must be assigned."); - Debug.Assert(!string.IsNullOrEmpty(_rawPath), "'rawPath' must have at least one character."); - - _rawOctets = new List(); - _requestUriString = new StringBuilder(); - _requestUriString.Append(_cookedUriScheme); - _requestUriString.Append(Uri.SchemeDelimiter); - _requestUriString.Append(_cookedUriHost); - - ParsingResult result = ParseRawPath(encoding); - if (result == ParsingResult.Success) - { - _requestUriString.Append(_cookedUriQuery); - - Debug.Assert(_rawOctets.Count == 0, - "Still raw octets left. They must be added to the result path."); - - if (!Uri.TryCreate(_requestUriString.ToString(), UriKind.Absolute, out _requestUri)) - { - // If we can't create a Uri from the string, this is an invalid string and it doesn't make - // sense to try another encoding. - result = ParsingResult.InvalidString; - } - } - - if (result != ParsingResult.Success) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_raw_path, _rawPath, encoding.EncodingName)); - } - - return result; - } - - private ParsingResult ParseRawPath(Encoding encoding) - { - Debug.Assert(encoding != null, "'encoding' must be assigned."); - - int index = 0; - char current = '\0'; - while (index < _rawPath.Length) - { - current = _rawPath[index]; - if (current == '%') - { - // Assert is enough, since http.sys accepted the request string already. This should never happen. - Debug.Assert(index + 2 < _rawPath.Length, "Expected >=2 characters after '%' (e.g. %2F)"); - - index++; - current = _rawPath[index]; - if (current == 'u' || current == 'U') - { - // We found "%u" which means, we have a Unicode code point of the form "%uXXXX". - Debug.Assert(index + 4 < _rawPath.Length, "Expected >=4 characters after '%u' (e.g. %u0062)"); - - // Decode the content of rawOctets into percent encoded UTF-8 characters and append them - // to requestUriString. - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - if (!AppendUnicodeCodePointValuePercentEncoded(_rawPath.Substring(index + 1, 4))) - { - return ParsingResult.InvalidString; - } - index += 5; - } - else - { - // We found '%', but not followed by 'u', i.e. we have a percent encoded octed: %XX - if (!AddPercentEncodedOctetToRawOctetsList(encoding, _rawPath.Substring(index, 2))) - { - return ParsingResult.InvalidString; - } - index += 2; - } - } - else - { - // We found a non-'%' character: decode the content of rawOctets into percent encoded - // UTF-8 characters and append it to the result. - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - // Append the current character to the result. - _requestUriString.Append(current); - index++; - } - } - - // if the raw path ends with a sequence of percent encoded octets, make sure those get added to the - // result (requestUriString). - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - - return ParsingResult.Success; - } - - private bool AppendUnicodeCodePointValuePercentEncoded(string codePoint) - { - // http.sys only supports %uXXXX (4 hex-digits), even though unicode code points could have up to - // 6 hex digits. Therefore we parse always 4 characters after %u and convert them to an int. - if (!int.TryParse(codePoint, NumberStyles.HexNumber, null, out var codePointValue)) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, codePoint)); - return false; - } - - string unicodeString = null; - try - { - unicodeString = char.ConvertFromUtf32(codePointValue); - AppendOctetsPercentEncoded(_requestUriString, s_utf8Encoding.GetBytes(unicodeString)); - - return true; - } - catch (ArgumentOutOfRangeException) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, codePoint)); - } - catch (EncoderFallbackException) - { - // If utf8Encoding.GetBytes() fails - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_to_utf8, unicodeString, e.Message)); - } - - return false; - } - - private bool AddPercentEncodedOctetToRawOctetsList(Encoding encoding, string escapedCharacter) - { - if (!byte.TryParse(escapedCharacter, NumberStyles.HexNumber, null, out byte encodedValue)) - { - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, escapedCharacter)); - return false; - } - - _rawOctets.Add(encodedValue); - - return true; - } - - private bool EmptyDecodeAndAppendRawOctetsList(Encoding encoding) - { - if (_rawOctets.Count == 0) - { - return true; - } - - string decodedString = null; - try - { - // If the encoding can get a string out of the byte array, this is a valid string in the - // 'encoding' encoding. - decodedString = encoding.GetString(_rawOctets.ToArray()); - - if (encoding == s_utf8Encoding) - { - AppendOctetsPercentEncoded(_requestUriString, _rawOctets.ToArray()); - } - else - { - AppendOctetsPercentEncoded(_requestUriString, s_utf8Encoding.GetBytes(decodedString)); - } - - _rawOctets.Clear(); - - return true; - } - catch (DecoderFallbackException) - { - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_bytes, GetOctetsAsString(_rawOctets), e.Message)); - } - catch (EncoderFallbackException) - { - // If utf8Encoding.GetBytes() fails - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_to_utf8, decodedString, e.Message)); - } - - return false; - } - - private static void AppendOctetsPercentEncoded(StringBuilder target, IEnumerable octets) - { - foreach (byte octet in octets) - { - target.Append('%'); - target.Append(octet.ToString("X2", CultureInfo.InvariantCulture)); - } - } - - private static string GetOctetsAsString(IEnumerable octets) - { - var octetString = new StringBuilder(); - - bool first = true; - foreach (byte octet in octets) - { - if (first) - { - first = false; - } - else - { - octetString.Append(' '); - } - octetString.Append(octet.ToString("X2", CultureInfo.InvariantCulture)); - } - - return octetString.ToString(); - } - - private static string GetPath(string uriString) - { - Debug.Assert(uriString != null, "uriString must not be null"); - Debug.Assert(uriString.Length > 0, "uriString must not be empty"); - - int pathStartIndex = 0; - - // Perf. improvement: nearly all strings are relative Uris. So just look if the - // string starts with '/'. If so, we have a relative Uri and the path starts at position 0. - // (http.sys already trimmed leading whitespaces) - if (uriString[0] != '/') - { - // We can't check against cookedUriScheme, since http.sys allows for request http://myserver/ to - // use a request line 'GET https://myserver/' (note http vs. https). Therefore check if the - // Uri starts with either http:// or https://. - int authorityStartIndex = 0; - if (uriString.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - { - authorityStartIndex = 7; - } - else if (uriString.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - authorityStartIndex = 8; - } - - if (authorityStartIndex > 0) - { - // we have an absolute Uri. Find out where the authority ends and the path begins. - // Note that Uris like "http://server?query=value/1/2" are invalid according to RFC2616 - // and http.sys behavior: If the Uri contains a query, there must be at least one '/' - // between the authority and the '?' character: It's safe to just look for the first - // '/' after the authority to determine the beginning of the path. - pathStartIndex = uriString.IndexOf('/', authorityStartIndex); - if (pathStartIndex == -1) - { - // e.g. for request lines like: 'GET http://myserver' (no final '/') - pathStartIndex = uriString.Length; - } - } - else - { - // RFC2616: Request-URI = "*" | absoluteURI | abs_path | authority - // 'authority' can only be used with CONNECT which is never received by HttpListener. - // I.e. if we don't have an absolute path (must start with '/') and we don't have - // an absolute Uri (must start with http:// or https://), then 'uriString' must be '*'. - Debug.Assert((uriString.Length == 1) && (uriString[0] == '*'), "Unknown request Uri string format", - "Request Uri string is not an absolute Uri, absolute path, or '*': {0}", uriString); - - // Should we ever get here, be consistent with 2.0/3.5 behavior: just add an initial - // slash to the string and treat it as a path: - uriString = "/" + uriString; - } - } - - // Find end of path: The path is terminated by - // - the first '?' character - // - the first '#' character: This is never the case here, since http.sys won't accept - // Uris containing fragments. Also, RFC2616 doesn't allow fragments in request Uris. - // - end of Uri string - int queryIndex = uriString.IndexOf('?'); - if (queryIndex == -1) - { - queryIndex = uriString.Length; - } - - // will always return a != null string. - return AddSlashToAsteriskOnlyPath(uriString.Substring(pathStartIndex, queryIndex - pathStartIndex)); - } - - private static string AddSlashToAsteriskOnlyPath(string path) - { - Debug.Assert(path != null, "'path' must not be null"); - - // If a request like "OPTIONS * HTTP/1.1" is sent to the listener, then the request Uri - // should be "http[s]://server[:port]/*" to be compatible with pre-4.0 behavior. - if ((path.Length == 1) && (path[0] == '*')) - { - return "/*"; - } - - return path; - } - - private enum ParsingResult - { - Success, - InvalidString, - EncodingError - } - - private enum EncodingType - { - Primary, - Secondary - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerResponse.Managed.cs b/SocketHttpListener/Net/HttpListenerResponse.Managed.cs deleted file mode 100644 index f595fce7cc..0000000000 --- a/SocketHttpListener/Net/HttpListenerResponse.Managed.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerResponse : IDisposable - { - private long _contentLength; - private Version _version = HttpVersion.Version11; - private int _statusCode = 200; - internal object _headersLock = new object(); - private bool _forceCloseChunked; - - internal HttpListenerResponse(HttpListenerContext context) - { - _httpContext = context; - } - - internal bool ForceCloseChunked => _forceCloseChunked; - - private void EnsureResponseStream() - { - if (_responseStream == null) - { - _responseStream = _httpContext.Connection.GetResponseStream(); - } - } - - public Version ProtocolVersion - { - get => _version; - set - { - CheckDisposed(); - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) - { - throw new ArgumentException("Wrong version"); - } - - _version = new Version(value.Major, value.Minor); // match Windows behavior, trimming to just Major.Minor - } - } - - public int StatusCode - { - get => _statusCode; - set - { - CheckDisposed(); - - if (value < 100 || value > 999) - throw new ProtocolViolationException("Invalid status"); - - _statusCode = value; - } - } - - private void Dispose() - { - Close(true); - } - - public void Close() - { - if (Disposed) - return; - - Close(false); - } - - public void Abort() - { - if (Disposed) - return; - - Close(true); - } - - private void Close(bool force) - { - Disposed = true; - _httpContext.Connection.Close(force); - } - - public void Close(byte[] responseEntity, bool willBlock) - { - CheckDisposed(); - - if (responseEntity == null) - { - throw new ArgumentNullException(nameof(responseEntity)); - } - - if (!SentHeaders && _boundaryType != BoundaryType.Chunked) - { - ContentLength64 = responseEntity.Length; - } - - if (willBlock) - { - try - { - OutputStream.Write(responseEntity, 0, responseEntity.Length); - } - finally - { - Close(false); - } - } - else - { - OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar => - { - var thisRef = (HttpListenerResponse)iar.AsyncState; - try - { - try - { - thisRef.OutputStream.EndWrite(iar); - } - finally - { - thisRef.Close(false); - } - } - catch (Exception) - { - // In case response was disposed during this time - } - }, this); - } - } - - public void CopyFrom(HttpListenerResponse templateResponse) - { - _webHeaders.Clear(); - //_webHeaders.Add(templateResponse._webHeaders); - _contentLength = templateResponse._contentLength; - _statusCode = templateResponse._statusCode; - _statusDescription = templateResponse._statusDescription; - _keepAlive = templateResponse._keepAlive; - _version = templateResponse._version; - } - - internal void SendHeaders(bool closing, MemoryStream ms, bool isWebSocketHandshake = false) - { - if (!isWebSocketHandshake) - { - if (_webHeaders["Server"] == null) - { - _webHeaders.Set("Server", "Microsoft-NetCore/2.0"); - } - - if (_webHeaders["Date"] == null) - { - _webHeaders.Set("Date", DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture)); - } - - if (_boundaryType == BoundaryType.None) - { - if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10) - { - _keepAlive = false; - } - else - { - _boundaryType = BoundaryType.Chunked; - } - - if (CanSendResponseBody(_httpContext.Response.StatusCode)) - { - _contentLength = -1; - } - else - { - _boundaryType = BoundaryType.ContentLength; - _contentLength = 0; - } - } - - if (_boundaryType != BoundaryType.Chunked) - { - if (_boundaryType != BoundaryType.ContentLength && closing) - { - _contentLength = CanSendResponseBody(_httpContext.Response.StatusCode) ? -1 : 0; - } - - if (_boundaryType == BoundaryType.ContentLength) - { - _webHeaders.Set("Content-Length", _contentLength.ToString("D", CultureInfo.InvariantCulture)); - } - } - - /* Apache forces closing the connection for these status codes: - * HttpStatusCode.BadRequest 400 - * HttpStatusCode.RequestTimeout 408 - * HttpStatusCode.LengthRequired 411 - * HttpStatusCode.RequestEntityTooLarge 413 - * HttpStatusCode.RequestUriTooLong 414 - * HttpStatusCode.InternalServerError 500 - * HttpStatusCode.ServiceUnavailable 503 - */ - bool conn_close = (_statusCode == (int)HttpStatusCode.BadRequest || _statusCode == (int)HttpStatusCode.RequestTimeout - || _statusCode == (int)HttpStatusCode.LengthRequired || _statusCode == (int)HttpStatusCode.RequestEntityTooLarge - || _statusCode == (int)HttpStatusCode.RequestUriTooLong || _statusCode == (int)HttpStatusCode.InternalServerError - || _statusCode == (int)HttpStatusCode.ServiceUnavailable); - - if (!conn_close) - { - conn_close = !_httpContext.Request.KeepAlive; - } - - // They sent both KeepAlive: true and Connection: close - if (!_keepAlive || conn_close) - { - _webHeaders.Set("Connection", "Close"); - conn_close = true; - } - - if (SendChunked) - { - _webHeaders.Set("Transfer-Encoding", "Chunked"); - } - - int reuses = _httpContext.Connection.Reuses; - if (reuses >= 100) - { - _forceCloseChunked = true; - if (!conn_close) - { - _webHeaders.Set("Connection", "Close"); - conn_close = true; - } - } - - if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10) - { - if (_keepAlive) - { - Headers["Keep-Alive"] = "true"; - } - - if (!conn_close) - { - _webHeaders.Set("Connection", "Keep-Alive"); - } - } - - ComputeCookies(); - } - - var encoding = Encoding.UTF8; - var writer = new StreamWriter(ms, encoding, 256); - writer.Write("HTTP/1.1 {0} ", _statusCode); // "1.1" matches Windows implementation, which ignores the response version - writer.Flush(); - byte[] statusDescriptionBytes = WebHeaderEncoding.GetBytes(StatusDescription); - ms.Write(statusDescriptionBytes, 0, statusDescriptionBytes.Length); - writer.Write("\r\n"); - - writer.Write(FormatHeaders(_webHeaders)); - writer.Flush(); - int preamble = encoding.GetPreamble().Length; - EnsureResponseStream(); - - /* Assumes that the ms was at position 0 */ - ms.Position = preamble; - SentHeaders = !isWebSocketHandshake; - } - - private static bool HeaderCanHaveEmptyValue(string name) => - !string.Equals(name, "Location", StringComparison.OrdinalIgnoreCase); - - private static string FormatHeaders(WebHeaderCollection headers) - { - var sb = new StringBuilder(); - - for (int i = 0; i < headers.Count; i++) - { - string key = headers.GetKey(i); - string[] values = headers.GetValues(i); - - int startingLength = sb.Length; - - sb.Append(key).Append(": "); - bool anyValues = false; - for (int j = 0; j < values.Length; j++) - { - string value = values[j]; - if (!string.IsNullOrWhiteSpace(value)) - { - if (anyValues) - { - sb.Append(", "); - } - sb.Append(value); - anyValues = true; - } - } - - if (anyValues || HeaderCanHaveEmptyValue(key)) - { - // Complete the header - sb.Append("\r\n"); - } - else - { - // Empty header; remove it. - sb.Length = startingLength; - } - } - - return sb.Append("\r\n").ToString(); - } - - private bool Disposed { get; set; } - internal bool SentHeaders { get; set; } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs deleted file mode 100644 index 66ef885643..0000000000 --- a/SocketHttpListener/Net/HttpListenerResponse.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerResponse : IDisposable - { - private BoundaryType _boundaryType = BoundaryType.None; - private CookieCollection _cookies; - private HttpListenerContext _httpContext; - private bool _keepAlive = true; - private HttpResponseStream _responseStream; - private string _statusDescription; - private WebHeaderCollection _webHeaders = new WebHeaderCollection(); - - public WebHeaderCollection Headers => _webHeaders; - - public Encoding ContentEncoding { get; set; } - - public string ContentType - { - get => Headers["Content-Type"]; - set - { - CheckDisposed(); - if (string.IsNullOrEmpty(value)) - { - Headers.Remove("Content-Type"); - } - else - { - Headers.Set("Content-Type", value); - } - } - } - - private HttpListenerContext HttpListenerContext => _httpContext; - - private HttpListenerRequest HttpListenerRequest => HttpListenerContext.Request; - - internal EntitySendFormat EntitySendFormat - { - get => (EntitySendFormat)_boundaryType; - set - { - CheckDisposed(); - CheckSentHeaders(); - if (value == EntitySendFormat.Chunked && HttpListenerRequest.ProtocolVersion.Minor == 0) - { - throw new ProtocolViolationException("net_nochunkuploadonhttp10"); - } - _boundaryType = (BoundaryType)value; - if (value != EntitySendFormat.ContentLength) - { - _contentLength = -1; - } - } - } - - public bool SendChunked - { - get => EntitySendFormat == EntitySendFormat.Chunked; - set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength; - } - - // We MUST NOT send message-body when we send responses with these Status codes - private static readonly int[] s_noResponseBody = { 100, 101, 204, 205, 304 }; - - private static bool CanSendResponseBody(int responseCode) - { - for (int i = 0; i < s_noResponseBody.Length; i++) - { - if (responseCode == s_noResponseBody[i]) - { - return false; - } - } - return true; - } - - public long ContentLength64 - { - get => _contentLength; - set - { - CheckDisposed(); - CheckSentHeaders(); - if (value >= 0) - { - _contentLength = value; - _boundaryType = BoundaryType.ContentLength; - } - else - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - } - } - - public CookieCollection Cookies - { - get => _cookies ?? (_cookies = new CookieCollection()); - set => _cookies = value; - } - - public bool KeepAlive - { - get => _keepAlive; - set - { - CheckDisposed(); - _keepAlive = value; - } - } - - public Stream OutputStream - { - get - { - CheckDisposed(); - EnsureResponseStream(); - return _responseStream; - } - } - - public string RedirectLocation - { - get => Headers["Location"]; - set - { - // note that this doesn't set the status code to a redirect one - CheckDisposed(); - if (string.IsNullOrEmpty(value)) - { - Headers.Remove("Location"); - } - else - { - Headers.Set("Location", value); - } - } - } - - public string StatusDescription - { - get - { - if (_statusDescription == null) - { - // if the user hasn't set this, generated on the fly, if possible. - // We know this one is safe, no need to verify it as in the setter. - _statusDescription = HttpStatusDescription.Get(StatusCode); - } - if (_statusDescription == null) - { - _statusDescription = string.Empty; - } - return _statusDescription; - } - set - { - CheckDisposed(); - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - // Need to verify the status description doesn't contain any control characters except HT. We mask off the high - // byte since that's how it's encoded. - for (int i = 0; i < value.Length; i++) - { - char c = (char)(0x000000ff & (uint)value[i]); - if ((c <= 31 && c != (byte)'\t') || c == 127) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); - } - } - - _statusDescription = value; - } - } - - public void AddHeader(string name, string value) - { - Headers.Set(name, value); - } - - public void AppendHeader(string name, string value) - { - Headers.Add(name, value); - } - - public void AppendCookie(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - Cookies.Add(cookie); - } - - private void ComputeCookies() - { - if (_cookies != null) - { - // now go through the collection, and concatenate all the cookies in per-variant strings - //string setCookie2 = null, setCookie = null; - //for (int index = 0; index < _cookies.Count; index++) - //{ - // Cookie cookie = _cookies[index]; - // string cookieString = cookie.ToServerString(); - // if (cookieString == null || cookieString.Length == 0) - // { - // continue; - // } - - // if (cookie.IsRfc2965Variant()) - // { - // setCookie2 = setCookie2 == null ? cookieString : setCookie2 + ", " + cookieString; - // } - // else - // { - // setCookie = setCookie == null ? cookieString : setCookie + ", " + cookieString; - // } - //} - - //if (!string.IsNullOrEmpty(setCookie)) - //{ - // Headers.Set(HttpKnownHeaderNames.SetCookie, setCookie); - // if (string.IsNullOrEmpty(setCookie2)) - // { - // Headers.Remove(HttpKnownHeaderNames.SetCookie2); - // } - //} - - //if (!string.IsNullOrEmpty(setCookie2)) - //{ - // Headers.Set(HttpKnownHeaderNames.SetCookie2, setCookie2); - // if (string.IsNullOrEmpty(setCookie)) - // { - // Headers.Remove(HttpKnownHeaderNames.SetCookie); - // } - //} - } - } - - public void Redirect(string url) - { - Headers["Location"] = url; - StatusCode = (int)HttpStatusCode.Redirect; - StatusDescription = "Found"; - } - - public void SetCookie(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - - //Cookie newCookie = cookie.Clone(); - //int added = Cookies.InternalAdd(newCookie, true); - - //if (added != 1) - //{ - // // The Cookie already existed and couldn't be replaced. - // throw new ArgumentException("Cookie exists"); - //} - } - - void IDisposable.Dispose() - { - Dispose(); - } - - private void CheckDisposed() - { - if (Disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - } - - private void CheckSentHeaders() - { - if (SentHeaders) - { - throw new InvalidOperationException(); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpRequestStream.Managed.cs b/SocketHttpListener/Net/HttpRequestStream.Managed.cs deleted file mode 100644 index 42fc4d97c2..0000000000 --- a/SocketHttpListener/Net/HttpRequestStream.Managed.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.IO; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpRequestStream : Stream - { - private byte[] _buffer; - private int _offset; - private int _length; - private long _remainingBody; - protected bool _closed; - private Stream _stream; - - internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length) - : this(stream, buffer, offset, length, -1) - { - } - - internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength) - { - _stream = stream; - _buffer = buffer; - _offset = offset; - _length = length; - _remainingBody = contentlength; - } - - // Returns 0 if we can keep reading from the base stream, - // > 0 if we read something from the buffer. - // -1 if we had a content length set and we finished reading that many bytes. - private int FillFromBuffer(byte[] buffer, int offset, int count) - { - if (_remainingBody == 0) - return -1; - - if (_length == 0) - return 0; - - int size = Math.Min(_length, count); - if (_remainingBody > 0) - size = (int)Math.Min(size, _remainingBody); - - if (_offset > _buffer.Length - size) - { - size = Math.Min(size, _buffer.Length - _offset); - } - if (size == 0) - return 0; - - Buffer.BlockCopy(_buffer, _offset, buffer, offset, size); - _offset += size; - _length -= size; - if (_remainingBody > 0) - _remainingBody -= size; - return size; - } - - protected virtual int ReadCore(byte[] buffer, int offset, int size) - { - // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 - int nread = FillFromBuffer(buffer, offset, size); - if (nread == -1) - { // No more bytes available (Content-Length) - return 0; - } - else if (nread > 0) - { - return nread; - } - - if (_remainingBody > 0) - { - size = (int)Math.Min(_remainingBody, (long)size); - } - - nread = _stream.Read(buffer, offset, size); - - if (_remainingBody > 0) - { - if (nread == 0) - { - throw new Exception("Bad request"); - } - - //Debug.Assert(nread <= _remainingBody); - _remainingBody -= nread; - } - - return nread; - } - - protected virtual IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - if (size == 0 || _closed) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - ares.Complete(); - return ares; - } - - int nread = FillFromBuffer(buffer, offset, size); - if (nread > 0 || nread == -1) - { - var ares = new HttpStreamAsyncResult(this); - ares._buffer = buffer; - ares._offset = offset; - ares._count = size; - ares._callback = cback; - ares._state = state; - ares._synchRead = Math.Max(0, nread); - ares.Complete(); - return ares; - } - - // Avoid reading past the end of the request to allow - // for HTTP pipelining - if (_remainingBody >= 0 && size > _remainingBody) - { - size = (int)Math.Min(_remainingBody, (long)size); - } - - return _stream.BeginRead(buffer, offset, size, cback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - if (asyncResult == null) - throw new ArgumentNullException(nameof(asyncResult)); - - var r = asyncResult as HttpStreamAsyncResult; - if (r != null) - { - if (!ReferenceEquals(this, r._parent)) - { - throw new ArgumentException("Invalid async result"); - } - if (r._endCalled) - { - throw new InvalidOperationException("invalid end call"); - } - r._endCalled = true; - - if (!asyncResult.IsCompleted) - { - asyncResult.AsyncWaitHandle.WaitOne(); - } - - return r._synchRead; - } - - if (_closed) - return 0; - - int nread = 0; - try - { - nread = _stream.EndRead(asyncResult); - } - catch (IOException e) when (e.InnerException is ArgumentException || e.InnerException is InvalidOperationException) - { - throw e.InnerException; - } - - if (_remainingBody > 0) - { - if (nread == 0) - { - throw new Exception("Bad request"); - } - - _remainingBody -= nread; - } - - return nread; - } - } -} diff --git a/SocketHttpListener/Net/HttpRequestStream.cs b/SocketHttpListener/Net/HttpRequestStream.cs deleted file mode 100644 index 1c554df20b..0000000000 --- a/SocketHttpListener/Net/HttpRequestStream.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpRequestStream : Stream - { - public override bool CanSeek => false; - public override bool CanWrite => false; - public override bool CanRead => true; - - public override int Read(byte[] buffer, int offset, int size) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - if (size == 0 || _closed) - { - return 0; - } - - return ReadCore(buffer, offset, size); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - return BeginReadCore(buffer, offset, size, callback, state); - } - - public override void Flush() { } - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return base.BeginWrite(buffer, offset, count, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - base.EndWrite(asyncResult); - } - - internal bool Closed => _closed; - - protected override void Dispose(bool disposing) - { - _closed = true; - base.Dispose(disposing); - } - } -} diff --git a/SocketHttpListener/Net/HttpResponseStream.Managed.cs b/SocketHttpListener/Net/HttpResponseStream.Managed.cs deleted file mode 100644 index 5d02a9c956..0000000000 --- a/SocketHttpListener/Net/HttpResponseStream.Managed.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpResponseStream : Stream - { - private HttpListenerResponse _response; - private bool _ignore_errors; - private bool _trailer_sent; - private Stream _stream; - private readonly IStreamHelper _streamHelper; - private readonly Socket _socket; - private readonly bool _supportsDirectSocketAccess; - private readonly IEnvironmentInfo _environment; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - - internal HttpResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IStreamHelper streamHelper, Socket socket, bool supportsDirectSocketAccess, IEnvironmentInfo environment, IFileSystem fileSystem, ILogger logger) - { - _response = response; - _ignore_errors = ignore_errors; - _streamHelper = streamHelper; - _socket = socket; - _supportsDirectSocketAccess = supportsDirectSocketAccess; - _environment = environment; - _fileSystem = fileSystem; - _logger = logger; - _stream = stream; - } - - private void DisposeCore() - { - byte[] bytes = null; - MemoryStream ms = GetHeaders(true); - bool chunked = _response.SendChunked; - if (_stream.CanWrite) - { - try - { - if (ms != null) - { - long start = ms.Position; - if (chunked && !_trailer_sent) - { - bytes = GetChunkSizeBytes(0, true); - ms.Position = ms.Length; - ms.Write(bytes, 0, bytes.Length); - } - InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); - _trailer_sent = true; - } - else if (chunked && !_trailer_sent) - { - bytes = GetChunkSizeBytes(0, true); - InternalWrite(bytes, 0, bytes.Length); - _trailer_sent = true; - } - } - catch (HttpListenerException) - { - // Ignore error due to connection reset by peer - } - } - _response.Close(); - } - - internal async Task WriteWebSocketHandshakeHeadersAsync() - { - if (_closed) - throw new ObjectDisposedException(GetType().ToString()); - - if (_stream.CanWrite) - { - MemoryStream ms = GetHeaders(closing: false, isWebSocketHandshake: true); - bool chunked = _response.SendChunked; - - long start = ms.Position; - if (chunked) - { - byte[] bytes = GetChunkSizeBytes(0, true); - ms.Position = ms.Length; - ms.Write(bytes, 0, bytes.Length); - } - - await InternalWriteAsync(ms.GetBuffer(), (int)start, (int)(ms.Length - start)).ConfigureAwait(false); - await _stream.FlushAsync().ConfigureAwait(false); - } - } - - private MemoryStream GetHeaders(bool closing, bool isWebSocketHandshake = false) - { - //// SendHeaders works on shared headers - //lock (_response.headers_lock) - //{ - // if (_response.HeadersSent) - // return null; - // var ms = CreateNew(); - // _response.SendHeaders(closing, ms); - // return ms; - //} - - // SendHeaders works on shared headers - lock (_response._headersLock) - { - if (_response.SentHeaders) - { - return null; - } - - MemoryStream ms = new MemoryStream(); - _response.SendHeaders(closing, ms, isWebSocketHandshake); - return ms; - } - } - - private static byte[] s_crlf = new byte[] { 13, 10 }; - private static byte[] GetChunkSizeBytes(int size, bool final) - { - string str = string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); - return Encoding.ASCII.GetBytes(str); - } - - internal void InternalWrite(byte[] buffer, int offset, int count) - { - if (_ignore_errors) - { - try - { - _stream.Write(buffer, offset, count); - } - catch { } - } - else - { - _stream.Write(buffer, offset, count); - } - } - - internal Task InternalWriteAsync(byte[] buffer, int offset, int count) => - _ignore_errors ? InternalWriteIgnoreErrorsAsync(buffer, offset, count) : _stream.WriteAsync(buffer, offset, count); - - private async Task InternalWriteIgnoreErrorsAsync(byte[] buffer, int offset, int count) - { - try { await _stream.WriteAsync(buffer, offset, count).ConfigureAwait(false); } - catch { } - } - - private void WriteCore(byte[] buffer, int offset, int size) - { - if (size == 0) - return; - - byte[] bytes = null; - MemoryStream ms = GetHeaders(false); - bool chunked = _response.SendChunked; - if (ms != null) - { - long start = ms.Position; // After the possible preamble for the encoding - ms.Position = ms.Length; - if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - ms.Write(bytes, 0, bytes.Length); - } - - int new_count = Math.Min(size, 16384 - (int)ms.Position + (int)start); - ms.Write(buffer, offset, new_count); - size -= new_count; - offset += new_count; - InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); - ms.SetLength(0); - ms.Capacity = 0; // 'dispose' the buffer in ms. - } - else if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - InternalWrite(bytes, 0, bytes.Length); - } - - if (size > 0) - InternalWrite(buffer, offset, size); - if (chunked) - InternalWrite(s_crlf, 0, 2); - } - - private IAsyncResult BeginWriteCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - if (_closed) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - ares.Complete(); - return ares; - } - - byte[] bytes = null; - MemoryStream ms = GetHeaders(false); - bool chunked = _response.SendChunked; - if (ms != null) - { - long start = ms.Position; - ms.Position = ms.Length; - if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - ms.Write(bytes, 0, bytes.Length); - } - ms.Write(buffer, offset, size); - buffer = ms.GetBuffer(); - offset = (int)start; - size = (int)(ms.Position - start); - } - else if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - InternalWrite(bytes, 0, bytes.Length); - } - - return _stream.BeginWrite(buffer, offset, size, cback, state); - } - - private void EndWriteCore(IAsyncResult asyncResult) - { - if (_closed) - return; - - if (_ignore_errors) - { - try - { - _stream.EndWrite(asyncResult); - if (_response.SendChunked) - _stream.Write(s_crlf, 0, 2); - } - catch { } - } - else - { - _stream.EndWrite(asyncResult); - if (_response.SendChunked) - _stream.Write(s_crlf, 0, 2); - } - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); - } - - const int StreamCopyToBufferSize = 81920; - private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - var allowAsync = _environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows; - - //if (count <= 0) - //{ - // allowAsync = true; - //} - - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsync) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - var targetStream = this; - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(targetStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpResponseStream.cs b/SocketHttpListener/Net/HttpResponseStream.cs deleted file mode 100644 index 085c2ad0cb..0000000000 --- a/SocketHttpListener/Net/HttpResponseStream.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - internal sealed partial class HttpResponseStream : Stream - { - private bool _closed; - internal bool Closed => _closed; - - public override bool CanRead => false; - public override bool CanSeek => false; - public override bool CanWrite => true; - - public override void Flush() { } - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return base.BeginRead(buffer, offset, count, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return base.EndRead(asyncResult); - } - - public override void Write(byte[] buffer, int offset, int size) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - if (_closed) - { - return; - } - - WriteCore(buffer, offset, size); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - return BeginWriteCore(buffer, offset, size, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - if (asyncResult == null) - { - throw new ArgumentNullException(nameof(asyncResult)); - } - - EndWriteCore(asyncResult); - } - - protected override void Dispose(bool disposing) - { - try - { - if (disposing) - { - if (_closed) - { - return; - } - _closed = true; - DisposeCore(); - } - } - finally - { - base.Dispose(disposing); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpStatusCode.cs b/SocketHttpListener/Net/HttpStatusCode.cs deleted file mode 100644 index d4bb61b8a7..0000000000 --- a/SocketHttpListener/Net/HttpStatusCode.cs +++ /dev/null @@ -1,321 +0,0 @@ -namespace SocketHttpListener.Net -{ - /// - /// Contains the values of the HTTP status codes. - /// - /// - /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in - /// RFC 2616 for HTTP 1.1. - /// - public enum HttpStatusCode - { - /// - /// Equivalent to status code 100. - /// Indicates that the client should continue with its request. - /// - Continue = 100, - /// - /// Equivalent to status code 101. - /// Indicates that the server is switching the HTTP version or protocol on the connection. - /// - SwitchingProtocols = 101, - /// - /// Equivalent to status code 200. - /// Indicates that the client's request has succeeded. - /// - OK = 200, - /// - /// Equivalent to status code 201. - /// Indicates that the client's request has been fulfilled and resulted in a new resource being - /// created. - /// - Created = 201, - /// - /// Equivalent to status code 202. - /// Indicates that the client's request has been accepted for processing, but the processing - /// hasn't been completed. - /// - Accepted = 202, - /// - /// Equivalent to status code 203. - /// Indicates that the returned metainformation is from a local or a third-party copy instead of - /// the origin server. - /// - NonAuthoritativeInformation = 203, - /// - /// Equivalent to status code 204. - /// Indicates that the server has fulfilled the client's request but doesn't need to return - /// an entity-body. - /// - NoContent = 204, - /// - /// Equivalent to status code 205. - /// Indicates that the server has fulfilled the client's request, and the user agent should - /// reset the document view which caused the request to be sent. - /// - ResetContent = 205, - /// - /// Equivalent to status code 206. - /// Indicates that the server has fulfilled the partial GET request for the resource. - /// - PartialContent = 206, - /// - /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. - /// - /// - /// MultipleChoices is a synonym for Ambiguous. - /// - /// - MultipleChoices = 300, - /// - /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. - /// - /// - /// Ambiguous is a synonym for MultipleChoices. - /// - /// - Ambiguous = 300, - /// - /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. - /// - /// - /// MovedPermanently is a synonym for Moved. - /// - /// - MovedPermanently = 301, - /// - /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. - /// - /// - /// Moved is a synonym for MovedPermanently. - /// - /// - Moved = 301, - /// - /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// Found is a synonym for Redirect. - /// - /// - Found = 302, - /// - /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// Redirect is a synonym for Found. - /// - /// - Redirect = 302, - /// - /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. - /// - /// - /// SeeOther is a synonym for RedirectMethod. - /// - /// - SeeOther = 303, - /// - /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. - /// - /// - /// RedirectMethod is a synonym for SeeOther. - /// - /// - RedirectMethod = 303, - /// - /// Equivalent to status code 304. - /// Indicates that the client has performed a conditional GET request and access is allowed, - /// but the document hasn't been modified. - /// - NotModified = 304, - /// - /// Equivalent to status code 305. - /// Indicates that the requested resource must be accessed through the proxy given by - /// the Location field. - /// - UseProxy = 305, - /// - /// Equivalent to status code 306. - /// This status code was used in a previous version of the specification, is no longer used, - /// and is reserved for future use. - /// - Unused = 306, - /// - /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// TemporaryRedirect is a synonym for RedirectKeepVerb. - /// - /// - TemporaryRedirect = 307, - /// - /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// RedirectKeepVerb is a synonym for TemporaryRedirect. - /// - /// - RedirectKeepVerb = 307, - /// - /// Equivalent to status code 400. - /// Indicates that the client's request couldn't be understood by the server due to - /// malformed syntax. - /// - BadRequest = 400, - /// - /// Equivalent to status code 401. - /// Indicates that the client's request requires user authentication. - /// - Unauthorized = 401, - /// - /// Equivalent to status code 402. - /// This status code is reserved for future use. - /// - PaymentRequired = 402, - /// - /// Equivalent to status code 403. - /// Indicates that the server understood the client's request but is refusing to fulfill it. - /// - Forbidden = 403, - /// - /// Equivalent to status code 404. - /// Indicates that the server hasn't found anything matching the request URI. - /// - NotFound = 404, - /// - /// Equivalent to status code 405. - /// Indicates that the method specified in the request line isn't allowed for the resource - /// identified by the request URI. - /// - MethodNotAllowed = 405, - /// - /// Equivalent to status code 406. - /// Indicates that the server doesn't have the appropriate resource to respond to the Accept - /// headers in the client's request. - /// - NotAcceptable = 406, - /// - /// Equivalent to status code 407. - /// Indicates that the client must first authenticate itself with the proxy. - /// - ProxyAuthenticationRequired = 407, - /// - /// Equivalent to status code 408. - /// Indicates that the client didn't produce a request within the time that the server was - /// prepared to wait. - /// - RequestTimeout = 408, - /// - /// Equivalent to status code 409. - /// Indicates that the client's request couldn't be completed due to a conflict on the server. - /// - Conflict = 409, - /// - /// Equivalent to status code 410. - /// Indicates that the requested resource is no longer available at the server and - /// no forwarding address is known. - /// - Gone = 410, - /// - /// Equivalent to status code 411. - /// Indicates that the server refuses to accept the client's request without a defined - /// Content-Length. - /// - LengthRequired = 411, - /// - /// Equivalent to status code 412. - /// Indicates that the precondition given in one or more of the request headers evaluated to - /// false when it was tested on the server. - /// - PreconditionFailed = 412, - /// - /// Equivalent to status code 413. - /// Indicates that the entity of the client's request is larger than the server is willing or - /// able to process. - /// - RequestEntityTooLarge = 413, - /// - /// Equivalent to status code 414. - /// Indicates that the request URI is longer than the server is willing to interpret. - /// - RequestUriTooLong = 414, - /// - /// Equivalent to status code 415. - /// Indicates that the entity of the client's request is in a format not supported by - /// the requested resource for the requested method. - /// - UnsupportedMediaType = 415, - /// - /// Equivalent to status code 416. - /// Indicates that none of the range specifier values in a Range request header overlap - /// the current extent of the selected resource. - /// - RequestedRangeNotSatisfiable = 416, - /// - /// Equivalent to status code 417. - /// Indicates that the expectation given in an Expect request header couldn't be met by - /// the server. - /// - ExpectationFailed = 417, - /// - /// Equivalent to status code 500. - /// Indicates that the server encountered an unexpected condition which prevented it from - /// fulfilling the client's request. - /// - InternalServerError = 500, - /// - /// Equivalent to status code 501. - /// Indicates that the server doesn't support the functionality required to fulfill the client's - /// request. - /// - NotImplemented = 501, - /// - /// Equivalent to status code 502. - /// Indicates that a gateway or proxy server received an invalid response from the upstream - /// server. - /// - BadGateway = 502, - /// - /// Equivalent to status code 503. - /// Indicates that the server is currently unable to handle the client's request due to - /// a temporary overloading or maintenance of the server. - /// - ServiceUnavailable = 503, - /// - /// Equivalent to status code 504. - /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream - /// server or some other auxiliary server. - /// - GatewayTimeout = 504, - /// - /// Equivalent to status code 505. - /// Indicates that the server doesn't support the HTTP version used in the client's request. - /// - HttpVersionNotSupported = 505, - } -} diff --git a/SocketHttpListener/Net/HttpStatusDescription.cs b/SocketHttpListener/Net/HttpStatusDescription.cs deleted file mode 100644 index a4e42560b9..0000000000 --- a/SocketHttpListener/Net/HttpStatusDescription.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static class HttpStatusDescription - { - internal static string Get(HttpStatusCode code) - { - return Get((int)code); - } - - internal static string Get(int code) - { - switch (code) - { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-Authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Request Entity Too Large"; - case 414: return "Request-Uri Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 426: return "Upgrade Required"; // RFC 2817 - - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "Http Version Not Supported"; - case 507: return "Insufficient Storage"; - } - return null; - } - } -} diff --git a/SocketHttpListener/Net/HttpStreamAsyncResult.cs b/SocketHttpListener/Net/HttpStreamAsyncResult.cs deleted file mode 100644 index 46944c624a..0000000000 --- a/SocketHttpListener/Net/HttpStreamAsyncResult.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - internal class HttpStreamAsyncResult : IAsyncResult - { - private object _locker = new object(); - private ManualResetEvent _handle; - private bool _completed; - - internal readonly object _parent; - internal byte[] _buffer; - internal int _offset; - internal int _count; - internal AsyncCallback _callback; - internal object _state; - internal int _synchRead; - internal Exception _error; - internal bool _endCalled; - - internal HttpStreamAsyncResult(object parent) - { - _parent = parent; - } - - public void Complete(Exception e) - { - _error = e; - Complete(); - } - - public void Complete() - { - lock (_locker) - { - if (_completed) - return; - - _completed = true; - if (_handle != null) - _handle.Set(); - - if (_callback != null) - Task.Run(() => _callback(this)); - } - } - - public object AsyncState => _state; - - public WaitHandle AsyncWaitHandle - { - get - { - lock (_locker) - { - if (_handle == null) - _handle = new ManualResetEvent(_completed); - } - - return _handle; - } - } - - public bool CompletedSynchronously => false; - - public bool IsCompleted - { - get - { - lock (_locker) - { - return _completed; - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpVersion.cs b/SocketHttpListener/Net/HttpVersion.cs deleted file mode 100644 index c0839b46d5..0000000000 --- a/SocketHttpListener/Net/HttpVersion.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace SocketHttpListener.Net -{ - // - // - public class HttpVersion - { - - public static readonly Version Version10 = new Version(1, 0); - public static readonly Version Version11 = new Version(1, 1); - - // pretty useless.. - public HttpVersion() { } - } -} diff --git a/SocketHttpListener/Net/ListenerPrefix.cs b/SocketHttpListener/Net/ListenerPrefix.cs deleted file mode 100644 index edfcb89043..0000000000 --- a/SocketHttpListener/Net/ListenerPrefix.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Net; - -namespace SocketHttpListener.Net -{ - internal sealed class ListenerPrefix - { - private string _original; - private string _host; - private ushort _port; - private string _path; - private bool _secure; - private IPAddress[] _addresses; - internal HttpListener _listener; - - public ListenerPrefix(string prefix) - { - _original = prefix; - Parse(prefix); - } - - public override string ToString() - { - return _original; - } - - public IPAddress[] Addresses - { - get => _addresses; - set => _addresses = value; - } - public bool Secure => _secure; - - public string Host => _host; - - public int Port => _port; - - public string Path => _path; - - // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. - public override bool Equals(object o) - { - var other = o as ListenerPrefix; - if (other == null) - return false; - - return (_original == other._original); - } - - public override int GetHashCode() - { - return _original.GetHashCode(); - } - - private void Parse(string uri) - { - ushort default_port = 80; - if (uri.StartsWith("https://")) - { - default_port = 443; - _secure = true; - } - - int length = uri.Length; - int start_host = uri.IndexOf(':') + 3; - if (start_host >= length) - throw new ArgumentException("net_listener_host"); - - int colon = uri.IndexOf(':', start_host, length - start_host); - int root; - if (colon > 0) - { - _host = uri.Substring(start_host, colon - start_host); - root = uri.IndexOf('/', colon, length - colon); - _port = (ushort)int.Parse(uri.Substring(colon + 1, root - colon - 1)); - _path = uri.Substring(root); - } - else - { - root = uri.IndexOf('/', start_host, length - start_host); - _host = uri.Substring(start_host, root - start_host); - _port = default_port; - _path = uri.Substring(root); - } - if (_path.Length != 1) - _path = _path.Substring(0, _path.Length - 1); - } - } -} diff --git a/SocketHttpListener/Net/UriScheme.cs b/SocketHttpListener/Net/UriScheme.cs deleted file mode 100644 index 33d1f09db6..0000000000 --- a/SocketHttpListener/Net/UriScheme.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static class UriScheme - { - public const string File = "file"; - public const string Ftp = "ftp"; - public const string Gopher = "gopher"; - public const string Http = "http"; - public const string Https = "https"; - public const string News = "news"; - public const string NetPipe = "net.pipe"; - public const string NetTcp = "net.tcp"; - public const string Nntp = "nntp"; - public const string Mailto = "mailto"; - public const string Ws = "ws"; - public const string Wss = "wss"; - - public const string SchemeDelimiter = "://"; - } -} diff --git a/SocketHttpListener/Net/WebHeaderCollection.cs b/SocketHttpListener/Net/WebHeaderCollection.cs deleted file mode 100644 index 34fca808be..0000000000 --- a/SocketHttpListener/Net/WebHeaderCollection.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net -{ - [ComVisible(true)] - public class WebHeaderCollection : QueryParamCollection - { - [Flags] - internal enum HeaderInfo - { - Request = 1, - Response = 1 << 1, - MultiValue = 1 << 10 - } - - static readonly bool[] allowed_chars = { - false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, - true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, - false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, - false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, - false, true, false - }; - - static readonly Dictionary headers; - - static WebHeaderCollection() - { - headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Allow", HeaderInfo.MultiValue }, - { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Accept-Charset", HeaderInfo.MultiValue }, - { "Accept-Encoding", HeaderInfo.MultiValue }, - { "Accept-Language", HeaderInfo.MultiValue }, - { "Accept-Ranges", HeaderInfo.MultiValue }, - { "Age", HeaderInfo.Response }, - { "Authorization", HeaderInfo.MultiValue }, - { "Cache-Control", HeaderInfo.MultiValue }, - { "Cookie", HeaderInfo.MultiValue }, - { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Content-Encoding", HeaderInfo.MultiValue }, - { "Content-Length", HeaderInfo.Request | HeaderInfo.Response }, - { "Content-Type", HeaderInfo.Request }, - { "Content-Language", HeaderInfo.MultiValue }, - { "Date", HeaderInfo.Request }, - { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue}, - { "Host", HeaderInfo.Request }, - { "If-Match", HeaderInfo.MultiValue }, - { "If-Modified-Since", HeaderInfo.Request }, - { "If-None-Match", HeaderInfo.MultiValue }, - { "Keep-Alive", HeaderInfo.Response }, - { "Pragma", HeaderInfo.MultiValue }, - { "Proxy-Authenticate", HeaderInfo.MultiValue }, - { "Proxy-Authorization", HeaderInfo.MultiValue }, - { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Range", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Referer", HeaderInfo.Request }, - { "Set-Cookie", HeaderInfo.MultiValue }, - { "Set-Cookie2", HeaderInfo.MultiValue }, - { "Server", HeaderInfo.Response }, - { "TE", HeaderInfo.MultiValue }, - { "Trailer", HeaderInfo.MultiValue }, - { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue }, - { "Translate", HeaderInfo.Request | HeaderInfo.Response }, - { "Upgrade", HeaderInfo.MultiValue }, - { "User-Agent", HeaderInfo.Request }, - { "Vary", HeaderInfo.MultiValue }, - { "Via", HeaderInfo.MultiValue }, - { "Warning", HeaderInfo.MultiValue }, - { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketAccept", HeaderInfo.Response }, - { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketKey", HeaderInfo.Request }, - { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue } - }; - } - - // Methods - - public void Add(string header) - { - if (header == null) - throw new ArgumentNullException(nameof(header)); - int pos = header.IndexOf(':'); - if (pos == -1) - throw new ArgumentException("no colon found", nameof(header)); - - this.Add(header.Substring(0, pos), header.Substring(pos + 1)); - } - - public override void Add(string name, string value) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - this.AddWithoutValidate(name, value); - } - - protected void AddWithoutValidate(string headerName, string headerValue) - { - if (!IsHeaderName(headerName)) - throw new ArgumentException("invalid header name: " + headerName, nameof(headerName)); - if (headerValue == null) - headerValue = string.Empty; - else - headerValue = headerValue.Trim(); - if (!IsHeaderValue(headerValue)) - throw new ArgumentException("invalid header value: " + headerValue, nameof(headerValue)); - - AddValue(headerName, headerValue); - } - - internal void AddValue(string headerName, string headerValue) - { - base.Add(headerName, headerValue); - } - - internal List GetValues_internal(string header, bool split) - { - if (header == null) - throw new ArgumentNullException(nameof(header)); - - var values = base.GetValues(header); - if (values == null || values.Count == 0) - return null; - - if (split && IsMultiValue(header)) - { - List separated = null; - foreach (var value in values) - { - if (value.IndexOf(',') < 0) - { - if (separated != null) - separated.Add(value); - - continue; - } - - if (separated == null) - { - separated = new List(values.Count + 1); - foreach (var v in values) - { - if (v == value) - break; - - separated.Add(v); - } - } - - var slices = value.Split(','); - var slices_length = slices.Length; - if (value[value.Length - 1] == ',') - --slices_length; - - for (int i = 0; i < slices_length; ++i) - { - separated.Add(slices[i].Trim()); - } - } - - if (separated != null) - return separated; - } - - return values; - } - - public override List GetValues(string header) - { - return GetValues_internal(header, true); - } - - public override string[] GetValues(int index) - { - string[] values = base.GetValues(index); - - if (values == null || values.Length == 0) - { - return null; - } - - return values; - } - - public static bool IsRestricted(string headerName) - { - return IsRestricted(headerName, false); - } - - public static bool IsRestricted(string headerName, bool response) - { - if (headerName == null) - throw new ArgumentNullException(nameof(headerName)); - - if (headerName.Length == 0) - throw new ArgumentException("empty string", nameof(headerName)); - - if (!IsHeaderName(headerName)) - throw new ArgumentException("Invalid character in header"); - - if (!headers.TryGetValue(headerName, out HeaderInfo info)) - return false; - - var flag = response ? HeaderInfo.Response : HeaderInfo.Request; - return (info & flag) != 0; - } - - public override void Set(string name, string value) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (!IsHeaderName(name)) - throw new ArgumentException("invalid header name"); - if (value == null) - value = string.Empty; - else - value = value.Trim(); - if (!IsHeaderValue(value)) - throw new ArgumentException("invalid header value"); - - base.Set(name, value); - } - - internal string ToStringMultiValue() - { - var sb = new StringBuilder(); - - int count = base.Count; - for (int i = 0; i < count; i++) - { - string key = GetKey(i); - if (IsMultiValue(key)) - { - foreach (string v in GetValues(i)) - { - sb.Append(key) - .Append(": ") - .Append(v) - .Append("\r\n"); - } - } - else - { - sb.Append(key) - .Append(": ") - .Append(Get(i)) - .Append("\r\n"); - } - } - return sb.Append("\r\n").ToString(); - } - - public override string ToString() - { - var sb = new StringBuilder(); - - int count = base.Count; - for (int i = 0; i < count; i++) - sb.Append(GetKey(i)) - .Append(": ") - .Append(Get(i)) - .Append("\r\n"); - - return sb.Append("\r\n").ToString(); - } - - - // Internal Methods - - // With this we don't check for invalid characters in header. See bug #55994. - internal void SetInternal(string header) - { - int pos = header.IndexOf(':'); - if (pos == -1) - throw new ArgumentException("no colon found", nameof(header)); - - SetInternal(header.Substring(0, pos), header.Substring(pos + 1)); - } - - internal void SetInternal(string name, string value) - { - if (value == null) - value = string.Empty; - else - value = value.Trim(); - if (!IsHeaderValue(value)) - throw new ArgumentException("invalid header value"); - - if (IsMultiValue(name)) - { - base.Add(name, value); - } - else - { - base.Remove(name); - base.Set(name, value); - } - } - - internal static bool IsMultiValue(string headerName) - { - if (headerName == null) - return false; - - return headers.TryGetValue(headerName, out HeaderInfo info) && (info & HeaderInfo.MultiValue) != 0; - } - - internal static bool IsHeaderValue(string value) - { - // TEXT any 8 bit value except CTL's (0-31 and 127) - // but including \r\n space and \t - // after a newline at least one space or \t must follow - // certain header fields allow comments () - - int len = value.Length; - for (int i = 0; i < len; i++) - { - char c = value[i]; - if (c == 127) - return false; - if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) - return false; - if (c == '\n' && ++i < len) - { - c = value[i]; - if (c != ' ' && c != '\t') - return false; - } - } - - return true; - } - - internal static bool IsHeaderName(string name) - { - if (name == null || name.Length == 0) - return false; - - int len = name.Length; - for (int i = 0; i < len; i++) - { - char c = name[i]; - if (c > 126 || !allowed_chars[c]) - return false; - } - - return true; - } - } -} diff --git a/SocketHttpListener/Net/WebHeaderEncoding.cs b/SocketHttpListener/Net/WebHeaderEncoding.cs deleted file mode 100644 index 96e0cc85d8..0000000000 --- a/SocketHttpListener/Net/WebHeaderEncoding.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text; - -namespace SocketHttpListener.Net -{ - // we use this static class as a helper class to encode/decode HTTP headers. - // what we need is a 1-1 correspondence between a char in the range U+0000-U+00FF - // and a byte in the range 0x00-0xFF (which is the range that can hit the network). - // The Latin-1 encoding (ISO-88591-1) (GetEncoding(28591)) works for byte[] to string, but is a little slow. - // It doesn't work for string -> byte[] because of best-fit-mapping problems. - internal static class WebHeaderEncoding - { - // We don't want '?' replacement characters, just fail. - private static readonly Encoding s_utf8Decoder = Encoding.GetEncoding("utf-8", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); - - internal static unsafe string GetString(byte[] bytes, int byteIndex, int byteCount) - { - fixed (byte* pBytes = bytes) - return GetString(pBytes + byteIndex, byteCount); - } - - internal static unsafe string GetString(byte* pBytes, int byteCount) - { - if (byteCount < 1) - return ""; - - string s = new string('\0', byteCount); - - fixed (char* pStr = s) - { - char* pString = pStr; - while (byteCount >= 8) - { - pString[0] = (char)pBytes[0]; - pString[1] = (char)pBytes[1]; - pString[2] = (char)pBytes[2]; - pString[3] = (char)pBytes[3]; - pString[4] = (char)pBytes[4]; - pString[5] = (char)pBytes[5]; - pString[6] = (char)pBytes[6]; - pString[7] = (char)pBytes[7]; - pString += 8; - pBytes += 8; - byteCount -= 8; - } - for (int i = 0; i < byteCount; i++) - { - pString[i] = (char)pBytes[i]; - } - } - - return s; - } - - internal static int GetByteCount(string myString) - { - return myString.Length; - } - internal static unsafe void GetBytes(string myString, int charIndex, int charCount, byte[] bytes, int byteIndex) - { - if (myString.Length == 0) - { - return; - } - fixed (byte* bufferPointer = bytes) - { - byte* newBufferPointer = bufferPointer + byteIndex; - int finalIndex = charIndex + charCount; - while (charIndex < finalIndex) - { - *newBufferPointer++ = (byte)myString[charIndex++]; - } - } - } - internal static byte[] GetBytes(string myString) - { - byte[] bytes = new byte[myString.Length]; - if (myString.Length != 0) - { - GetBytes(myString, 0, myString.Length, bytes, 0); - } - return bytes; - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs deleted file mode 100644 index 5ed49ec47d..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Security.Principal; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net.WebSockets -{ - public class HttpListenerWebSocketContext : WebSocketContext - { - private readonly Uri _requestUri; - private readonly QueryParamCollection _headers; - private readonly CookieCollection _cookieCollection; - private readonly IPrincipal _user; - private readonly bool _isAuthenticated; - private readonly bool _isLocal; - private readonly bool _isSecureConnection; - - private readonly string _origin; - private readonly IEnumerable _secWebSocketProtocols; - private readonly string _secWebSocketVersion; - private readonly string _secWebSocketKey; - - private readonly WebSocket _webSocket; - - internal HttpListenerWebSocketContext( - Uri requestUri, - QueryParamCollection headers, - CookieCollection cookieCollection, - IPrincipal user, - bool isAuthenticated, - bool isLocal, - bool isSecureConnection, - string origin, - IEnumerable secWebSocketProtocols, - string secWebSocketVersion, - string secWebSocketKey, - WebSocket webSocket) - { - _cookieCollection = new CookieCollection(); - _cookieCollection.Add(cookieCollection); - - //_headers = new NameValueCollection(headers); - _headers = headers; - _user = CopyPrincipal(user); - - _requestUri = requestUri; - _isAuthenticated = isAuthenticated; - _isLocal = isLocal; - _isSecureConnection = isSecureConnection; - _origin = origin; - _secWebSocketProtocols = secWebSocketProtocols; - _secWebSocketVersion = secWebSocketVersion; - _secWebSocketKey = secWebSocketKey; - _webSocket = webSocket; - } - - public override Uri RequestUri => _requestUri; - - public override QueryParamCollection Headers => _headers; - - public override string Origin => _origin; - - public override IEnumerable SecWebSocketProtocols => _secWebSocketProtocols; - - public override string SecWebSocketVersion => _secWebSocketVersion; - - public override string SecWebSocketKey => _secWebSocketKey; - - public override CookieCollection CookieCollection => _cookieCollection; - - public override IPrincipal User => _user; - - public override bool IsAuthenticated => _isAuthenticated; - - public override bool IsLocal => _isLocal; - - public override bool IsSecureConnection => _isSecureConnection; - - public override WebSocket WebSocket => _webSocket; - - private static IPrincipal CopyPrincipal(IPrincipal user) - { - if (user != null) - { - throw new NotImplementedException(); - } - - return null; - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs b/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs deleted file mode 100644 index 1cfd2dc902..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class HttpWebSocket - { - private const string SupportedVersion = "13"; - - internal static async Task AcceptWebSocketAsyncCore(HttpListenerContext context, - string subProtocol, - int receiveBufferSize, - TimeSpan keepAliveInterval, - ArraySegment? internalBuffer = null) - { - ValidateOptions(subProtocol, receiveBufferSize, MinSendBufferSize, keepAliveInterval); - - // get property will create a new response if one doesn't exist. - HttpListenerResponse response = context.Response; - HttpListenerRequest request = context.Request; - ValidateWebSocketHeaders(context); - - string secWebSocketVersion = request.Headers[HttpKnownHeaderNames.SecWebSocketVersion]; - - // Optional for non-browser client - string origin = request.Headers[HttpKnownHeaderNames.Origin]; - - string[] secWebSocketProtocols = null; - bool shouldSendSecWebSocketProtocolHeader = - ProcessWebSocketProtocolHeader( - request.Headers[HttpKnownHeaderNames.SecWebSocketProtocol], - subProtocol, - out var outgoingSecWebSocketProtocolString); - - if (shouldSendSecWebSocketProtocolHeader) - { - secWebSocketProtocols = new string[] { outgoingSecWebSocketProtocolString }; - response.Headers.Add(HttpKnownHeaderNames.SecWebSocketProtocol, outgoingSecWebSocketProtocolString); - } - - // negotiate the websocket key return value - string secWebSocketKey = request.Headers[HttpKnownHeaderNames.SecWebSocketKey]; - string secWebSocketAccept = HttpWebSocket.GetSecWebSocketAcceptString(secWebSocketKey); - - response.Headers.Add(HttpKnownHeaderNames.Connection, HttpKnownHeaderNames.Upgrade); - response.Headers.Add(HttpKnownHeaderNames.Upgrade, WebSocketUpgradeToken); - response.Headers.Add(HttpKnownHeaderNames.SecWebSocketAccept, secWebSocketAccept); - - response.StatusCode = (int)HttpStatusCode.SwitchingProtocols; // HTTP 101 - response.StatusDescription = HttpStatusDescription.Get(HttpStatusCode.SwitchingProtocols); - - var responseStream = response.OutputStream as HttpResponseStream; - - // Send websocket handshake headers - await responseStream.WriteWebSocketHandshakeHeadersAsync().ConfigureAwait(false); - - //WebSocket webSocket = WebSocket.CreateFromStream(context.Connection.ConnectedStream, isServer: true, subProtocol, keepAliveInterval); - var webSocket = new WebSocket(subProtocol); - - var webSocketContext = new HttpListenerWebSocketContext( - request.Url, - request.Headers, - request.Cookies, - context.User, - request.IsAuthenticated, - request.IsLocal, - request.IsSecureConnection, - origin, - secWebSocketProtocols != null ? secWebSocketProtocols : Array.Empty(), - secWebSocketVersion, - secWebSocketKey, - webSocket); - - webSocket.SetContext(webSocketContext, context.Connection.Close, context.Connection.Stream); - - return webSocketContext; - } - - private const bool WebSocketsSupported = true; - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs b/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs deleted file mode 100644 index b346cc98ec..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class HttpWebSocket - { - internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - internal const string WebSocketUpgradeToken = "websocket"; - internal const int DefaultReceiveBufferSize = 16 * 1024; - internal const int DefaultClientSendBufferSize = 16 * 1024; - - [SuppressMessage("Microsoft.Security", "CA5350", Justification = "SHA1 used only for hashing purposes, not for crypto.")] - internal static string GetSecWebSocketAcceptString(string secWebSocketKey) - { - string retVal; - - // SHA1 used only for hashing purposes, not for crypto. Check here for FIPS compat. - using (var sha1 = SHA1.Create()) - { - string acceptString = string.Concat(secWebSocketKey, HttpWebSocket.SecWebSocketKeyGuid); - byte[] toHash = Encoding.UTF8.GetBytes(acceptString); - retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); - } - - return retVal; - } - - // return value here signifies if a Sec-WebSocket-Protocol header should be returned by the server. - internal static bool ProcessWebSocketProtocolHeader(string clientSecWebSocketProtocol, - string subProtocol, - out string acceptProtocol) - { - acceptProtocol = string.Empty; - if (string.IsNullOrEmpty(clientSecWebSocketProtocol)) - { - // client hasn't specified any Sec-WebSocket-Protocol header - if (subProtocol != null) - { - // If the server specified _anything_ this isn't valid. - throw new WebSocketException("UnsupportedProtocol"); - } - // Treat empty and null from the server as the same thing here, server should not send headers. - return false; - } - - // here, we know the client specified something and it's non-empty. - - if (subProtocol == null) - { - // client specified some protocols, server specified 'null'. So server should send headers. - return true; - } - - // here, we know that the client has specified something, it's not empty - // and the server has specified exactly one protocol - - string[] requestProtocols = clientSecWebSocketProtocol.Split(new char[] { ',' }, - StringSplitOptions.RemoveEmptyEntries); - acceptProtocol = subProtocol; - - // client specified protocols, serverOptions has exactly 1 non-empty entry. Check that - // this exists in the list the client specified. - for (int i = 0; i < requestProtocols.Length; i++) - { - string currentRequestProtocol = requestProtocols[i].Trim(); - if (string.Equals(acceptProtocol, currentRequestProtocol, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - throw new WebSocketException("net_WebSockets_AcceptUnsupportedProtocol"); - } - - internal static void ValidateOptions(string subProtocol, int receiveBufferSize, int sendBufferSize, TimeSpan keepAliveInterval) - { - if (subProtocol != null) - { - WebSocketValidate.ValidateSubprotocol(subProtocol); - } - - if (receiveBufferSize < MinReceiveBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), "The receiveBufferSize was too small."); - } - - if (sendBufferSize < MinSendBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(sendBufferSize), "The sendBufferSize was too small."); - } - - if (receiveBufferSize > MaxBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), "The receiveBufferSize was too large."); - } - - if (sendBufferSize > MaxBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(sendBufferSize), "The sendBufferSize was too large."); - } - - if (keepAliveInterval < Timeout.InfiniteTimeSpan) // -1 millisecond - { - throw new ArgumentOutOfRangeException(nameof(keepAliveInterval), "The keepAliveInterval was too small."); - } - } - - internal const int MinSendBufferSize = 16; - internal const int MinReceiveBufferSize = 256; - internal const int MaxBufferSize = 64 * 1024; - - private static void ValidateWebSocketHeaders(HttpListenerContext context) - { - if (!WebSocketsSupported) - { - throw new PlatformNotSupportedException("net_WebSockets_UnsupportedPlatform"); - } - - if (!context.Request.IsWebSocketRequest) - { - throw new WebSocketException("net_WebSockets_AcceptNotAWebSocket"); - } - - string secWebSocketVersion = context.Request.Headers[HttpKnownHeaderNames.SecWebSocketVersion]; - if (string.IsNullOrEmpty(secWebSocketVersion)) - { - throw new WebSocketException("net_WebSockets_AcceptHeaderNotFound"); - } - - if (!string.Equals(secWebSocketVersion, SupportedVersion, StringComparison.OrdinalIgnoreCase)) - { - throw new WebSocketException("net_WebSockets_AcceptUnsupportedWebSocketVersion"); - } - - string secWebSocketKey = context.Request.Headers[HttpKnownHeaderNames.SecWebSocketKey]; - bool isSecWebSocketKeyInvalid = string.IsNullOrWhiteSpace(secWebSocketKey); - if (!isSecWebSocketKeyInvalid) - { - try - { - // key must be 16 bytes then base64-encoded - isSecWebSocketKeyInvalid = Convert.FromBase64String(secWebSocketKey).Length != 16; - } - catch - { - isSecWebSocketKeyInvalid = true; - } - } - if (isSecWebSocketKeyInvalid) - { - throw new WebSocketException("net_WebSockets_AcceptHeaderNotFound"); - } - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs b/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs deleted file mode 100644 index 5ac89cf480..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace SocketHttpListener.Net.WebSockets -{ - public enum WebSocketCloseStatus - { - NormalClosure = 1000, - EndpointUnavailable = 1001, - ProtocolError = 1002, - InvalidMessageType = 1003, - Empty = 1005, - // AbnormalClosure = 1006, // 1006 is reserved and should never be used by user - InvalidPayloadData = 1007, - PolicyViolation = 1008, - MessageTooBig = 1009, - MandatoryExtension = 1010, - InternalServerError = 1011 - // TLSHandshakeFailed = 1015, // 1015 is reserved and should never be used by user - - // 0 - 999 Status codes in the range 0-999 are not used. - // 1000 - 1999 Status codes in the range 1000-1999 are reserved for definition by this protocol. - // 2000 - 2999 Status codes in the range 2000-2999 are reserved for use by extensions. - // 3000 - 3999 Status codes in the range 3000-3999 MAY be used by libraries and frameworks. The - // interpretation of these codes is undefined by this protocol. End applications MUST - // NOT use status codes in this range. - // 4000 - 4999 Status codes in the range 4000-4999 MAY be used by application code. The interpretation - // of these codes is undefined by this protocol. - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener/Net/WebSockets/WebSocketContext.cs deleted file mode 100644 index 10ad864398..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Security.Principal; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net.WebSockets -{ - public abstract class WebSocketContext - { - public abstract Uri RequestUri { get; } - public abstract QueryParamCollection Headers { get; } - public abstract string Origin { get; } - public abstract IEnumerable SecWebSocketProtocols { get; } - public abstract string SecWebSocketVersion { get; } - public abstract string SecWebSocketKey { get; } - public abstract CookieCollection CookieCollection { get; } - public abstract IPrincipal User { get; } - public abstract bool IsAuthenticated { get; } - public abstract bool IsLocal { get; } - public abstract bool IsSecureConnection { get; } - public abstract WebSocket WebSocket { get; } - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs b/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs deleted file mode 100644 index 0469e3b6c5..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class WebSocketValidate - { - internal const int MaxControlFramePayloadLength = 123; - private const int CloseStatusCodeAbort = 1006; - private const int CloseStatusCodeFailedTLSHandshake = 1015; - private const int InvalidCloseStatusCodesFrom = 0; - private const int InvalidCloseStatusCodesTo = 999; - private const string Separators = "()<>@,;:\\\"/[]?={} "; - - internal static void ThrowIfInvalidState(WebSocketState currentState, bool isDisposed, WebSocketState[] validStates) - { - string validStatesText = string.Empty; - - if (validStates != null && validStates.Length > 0) - { - foreach (WebSocketState validState in validStates) - { - if (currentState == validState) - { - // Ordering is important to maintain .NET 4.5 WebSocket implementation exception behavior. - if (isDisposed) - { - throw new ObjectDisposedException(nameof(WebSocket)); - } - - return; - } - } - - validStatesText = string.Join(", ", validStates); - } - - throw new WebSocketException("net_WebSockets_InvalidState"); - } - - internal static void ValidateSubprotocol(string subProtocol) - { - if (string.IsNullOrWhiteSpace(subProtocol)) - { - throw new ArgumentException("net_WebSockets_InvalidEmptySubProtocol"); - } - - string invalidChar = null; - int i = 0; - while (i < subProtocol.Length) - { - char ch = subProtocol[i]; - if (ch < 0x21 || ch > 0x7e) - { - invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); - break; - } - - if (!char.IsLetterOrDigit(ch) && - Separators.IndexOf(ch) >= 0) - { - invalidChar = ch.ToString(); - break; - } - - i++; - } - - if (invalidChar != null) - { - throw new ArgumentException("net_WebSockets_InvalidCharInProtocolString"); - } - } - - internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, string statusDescription) - { - if (closeStatus == WebSocketCloseStatus.Empty && !string.IsNullOrEmpty(statusDescription)) - { - throw new ArgumentException("net_WebSockets_ReasonNotNull"); - } - - int closeStatusCode = (int)closeStatus; - - if ((closeStatusCode >= InvalidCloseStatusCodesFrom && - closeStatusCode <= InvalidCloseStatusCodesTo) || - closeStatusCode == CloseStatusCodeAbort || - closeStatusCode == CloseStatusCodeFailedTLSHandshake) - { - // CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort - throw new ArgumentException("net_WebSockets_InvalidCloseStatusCode"); - } - - int length = 0; - if (!string.IsNullOrEmpty(statusDescription)) - { - length = Encoding.UTF8.GetByteCount(statusDescription); - } - - if (length > MaxControlFramePayloadLength) - { - throw new ArgumentException("net_WebSockets_InvalidCloseStatusDescription"); - } - } - - internal static void ValidateArraySegment(ArraySegment arraySegment, string parameterName) - { - if (arraySegment.Array == null) - { - throw new ArgumentNullException(parameterName + "." + nameof(arraySegment.Array)); - } - if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) - { - throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Offset)); - } - if (arraySegment.Count < 0 || arraySegment.Count > (arraySegment.Array.Length - arraySegment.Offset)) - { - throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Count)); - } - } - - internal static void ValidateBuffer(byte[] buffer, int offset, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > (buffer.Length - offset)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - } - } -} diff --git a/SocketHttpListener/WebSocket.cs b/SocketHttpListener/WebSocket.cs index 0dcb6a64bc..afce871fd6 100644 --- a/SocketHttpListener/WebSocket.cs +++ b/SocketHttpListener/WebSocket.cs @@ -4,11 +4,10 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; +using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; using WebSocketState = System.Net.WebSockets.WebSocketState; namespace SocketHttpListener From 33b67a357fe73103f046b822d7cc66fc5318a9ef Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 21 Feb 2019 12:32:47 +0100 Subject: [PATCH 0072/6208] Remove unused deps --- Jellyfin.Server/CoreAppHost.cs | 11 +------ .../SocketSharp/WebSocketSharpListener.cs | 32 +------------------ 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 84d78d3fb6..68fd2453e3 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -47,15 +47,6 @@ namespace Jellyfin.Server protected override void ShutdownInternal() => Program.Shutdown(); protected override IHttpListener CreateHttpListener() - => new WebSocketSharpListener( - Logger, - Certificate, - StreamHelper, - NetworkManager, - SocketFactory, - CryptographyProvider, - SupportsDualModeSockets, - FileSystemManager, - EnvironmentInfo); + => new WebSocketSharpListener(Logger); } } diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index 64856f04af..c458fdb78f 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -1,18 +1,12 @@ using System; using System.Collections.Generic; using System.Net; - using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.Net; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.SocketSharp @@ -22,38 +16,14 @@ using Microsoft.Extensions.Logging; private HttpListener _listener; private readonly ILogger _logger; - private readonly X509Certificate _certificate; - private readonly IStreamHelper _streamHelper; - private readonly INetworkManager _networkManager; - private readonly ISocketFactory _socketFactory; - private readonly ICryptoProvider _cryptoProvider; - private readonly IFileSystem _fileSystem; - private readonly bool _enableDualMode; - private readonly IEnvironmentInfo _environment; private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationToken _disposeCancellationToken; public WebSocketSharpListener( - ILogger logger, - X509Certificate certificate, - IStreamHelper streamHelper, - INetworkManager networkManager, - ISocketFactory socketFactory, - ICryptoProvider cryptoProvider, - bool enableDualMode, - IFileSystem fileSystem, - IEnvironmentInfo environment) + ILogger logger) { _logger = logger; - _certificate = certificate; - _streamHelper = streamHelper; - _networkManager = networkManager; - _socketFactory = socketFactory; - _cryptoProvider = cryptoProvider; - _enableDualMode = enableDualMode; - _fileSystem = fileSystem; - _environment = environment; _disposeCancellationToken = _disposeCancellationTokenSource.Token; } From 852460b99155e015ed5f1d7ad2fab0281bfdfbec Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 25 Feb 2019 23:34:32 +0100 Subject: [PATCH 0073/6208] kestrel init --- .../ApplicationHost.cs | 119 ++- .../Emby.Server.Implementations.csproj | 10 + .../HttpServer/HttpListenerHost.cs | 2 +- .../SocketSharp/HttpFile.cs | 18 + .../SocketSharp/HttpPostedFile.cs | 204 ++++++ .../SocketSharp/RequestMono.cs | 681 ++++++++++++++++++ .../SocketSharp/SharpWebSocket.cs | 149 ++++ .../SocketSharp/WebSocketSharpListener.cs | 261 +++++++ .../SocketSharp/WebSocketSharpRequest.cs | 539 ++++++++++++++ .../SocketSharp/WebSocketSharpResponse.cs | 206 ++++++ Emby.Server.Implementations/Startup.cs | 34 + Jellyfin.Server/Program.cs | 3 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 4 +- .../Services/QueryParamCollection.cs | 17 + 14 files changed, 2242 insertions(+), 5 deletions(-) create mode 100644 Emby.Server.Implementations/SocketSharp/HttpFile.cs create mode 100644 Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs create mode 100644 Emby.Server.Implementations/SocketSharp/RequestMono.cs create mode 100644 Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs create mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs create mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs create mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs create mode 100644 Emby.Server.Implementations/Startup.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 45a819f260..5e8611d531 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -42,6 +42,7 @@ using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Xml; @@ -105,10 +106,15 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using ServiceStack; +using HttpResponse = MediaBrowser.Model.Net.HttpResponse; using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; namespace Emby.Server.Implementations @@ -123,7 +129,7 @@ namespace Emby.Server.Implementations ///
/// true if this instance can self restart; otherwise, false. public abstract bool CanSelfRestart { get; } - + public IWebHost Host { get; set; } public virtual bool CanLaunchWebBrowser { get @@ -612,6 +618,115 @@ namespace Emby.Server.Implementations await RegisterResources(serviceCollection); FindParts(); + + Host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot("/Users/clausvium/RiderProjects/jellyfin/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin-web/src") + .UseStartup() +// .ConfigureServices(async services => +// { +// services.AddSingleton(startUp); +// RegisterResources(services); +// FindParts(); +// try +// { +// ImageProcessor.ImageEncoder = +// new NullImageEncoder(); //SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager); +// } +// catch (Exception ex) +// { +// Logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}"); +// ImageProcessor.ImageEncoder = new NullImageEncoder(); +// } +// await RunStartupTasks().ConfigureAwait(false); +// }) + .UseUrls("http://localhost:8096") + .ConfigureServices(s => s.AddRouting()) + .Configure( app => + { + app.UseWebSockets(new WebSocketOptions { + KeepAliveInterval = TimeSpan.FromMilliseconds(1000000000), + ReceiveBufferSize = 0x10000 + }); + + app.UseRouter(r => + { + // TODO all the verbs, but really MVC... + r.MapGet("/{*localpath}", ExecuteHandler); + r.MapPut("/{*localpath}", ExecuteHandler); + r.MapPost("/{*localpath}", ExecuteHandler); + r.MapDelete("/{*localpath}", ExecuteHandler); + r.MapVerb("HEAD", "/{*localpath}", ExecuteHandler); + }); + }) + .Build(); + } + + public async Task ExecuteHandler(HttpRequest request, Microsoft.AspNetCore.Http.HttpResponse response, RouteData data) + { + var ctx = request.HttpContext; + if (ctx.WebSockets.IsWebSocketRequest) + { + try + { + var endpoint = ctx.Request.Path.ToString(); + var url = ctx.Request.Path.ToString(); + + var queryString = new QueryParamCollection(request.Query); + + var connectingArgs = new WebSocketConnectingEventArgs + { + Url = url, + QueryString = queryString, + Endpoint = endpoint + }; + + if (connectingArgs.AllowConnection) + { + Logger.LogDebug("Web socket connection allowed"); + + var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; + + //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); + //socket.ConnectAsServerAsync().ConfigureAwait(false); + +// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) +// { +// OnReceive = ProcessWebSocketMessageReceived, +// Url = e.Url, +// QueryString = e.QueryString ?? new QueryParamCollection() +// }; +// +// connection.Closed += Connection_Closed; +// +// lock (_webSocketConnections) +// { +// _webSocketConnections.Add(connection); +// } +// +// WebSocketConnected(new WebSocketConnectEventArgs +// { +// Url = url, +// QueryString = queryString, +// WebSocket = socket, +// Endpoint = endpoint +// }); + await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); + } + else + { + Logger.LogWarning("Web socket connection not allowed"); + ctx.Response.StatusCode = 401; + } + } + catch (Exception ex) + { + ctx.Response.StatusCode = 500; + } + } + + var req = new WebSocketSharpRequest(request, response, request.Path, Logger); + await ((HttpListenerHost)HttpServer).RequestHandler(req,request.Path.ToString(), request.Host.ToString(), data.Values["localpath"].ToString(), CancellationToken.None).ConfigureAwait(false); } protected virtual IHttpClient CreateHttpClient() @@ -1067,7 +1182,7 @@ namespace Emby.Server.Implementations HttpServer.Init(GetExports(false), GetExports()); - StartServer(); + //StartServer(); LibraryManager.AddParts(GetExports(), GetExports(), diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index bbf165d627..acbc60c39e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,6 +22,16 @@ + + + + + + + + + + diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index fb42460f1b..1bd084259d 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -422,7 +422,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// Overridable method that can be used to implement a custom hnandler /// - protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) + public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); diff --git a/Emby.Server.Implementations/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs new file mode 100644 index 0000000000..120ac50d9c --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/HttpFile.cs @@ -0,0 +1,18 @@ +using System.IO; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.SocketSharp +{ + public class HttpFile : IHttpFile + { + public string Name { get; set; } + + public string FileName { get; set; } + + public long ContentLength { get; set; } + + public string ContentType { get; set; } + + public Stream InputStream { get; set; } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs new file mode 100644 index 0000000000..f38ed848ee --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +public sealed class HttpPostedFile : IDisposable +{ + private string _name; + private string _contentType; + private Stream _stream; + private bool _disposed = false; + + internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) + { + _name = name; + _contentType = content_type; + _stream = new ReadSubStream(base_stream, offset, length); + } + + public string ContentType => _contentType; + + public int ContentLength => (int)_stream.Length; + + public string FileName => _name; + + public Stream InputStream => _stream; + + /// + /// Releases the unmanaged resources and disposes of the managed resources used. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _stream.Dispose(); + _stream = null; + + _name = null; + _contentType = null; + + _disposed = true; + } + + private class ReadSubStream : Stream + { + private Stream _stream; + private long _offset; + private long _end; + private long _position; + + public ReadSubStream(Stream s, long offset, long length) + { + _stream = s; + _offset = offset; + _end = offset + length; + _position = offset; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int dest_offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (dest_offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "< 0"); + } + + int len = buffer.Length; + if (dest_offset > len) + { + throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset)); + } + + // reordered to avoid possible integer overflow + if (dest_offset > len - count) + { + throw new ArgumentException("Reading would overrun buffer", nameof(count)); + } + + if (count > _end - _position) + { + count = (int)(_end - _position); + } + + if (count <= 0) + { + return 0; + } + + _stream.Position = _position; + int result = _stream.Read(buffer, dest_offset, count); + if (result > 0) + { + _position += result; + } + else + { + _position = _end; + } + + return result; + } + + public override int ReadByte() + { + if (_position >= _end) + { + return -1; + } + + _stream.Position = _position; + int result = _stream.ReadByte(); + if (result < 0) + { + _position = _end; + } + else + { + _position++; + } + + return result; + } + + public override long Seek(long d, SeekOrigin origin) + { + long real; + switch (origin) + { + case SeekOrigin.Begin: + real = _offset + d; + break; + case SeekOrigin.End: + real = _end + d; + break; + case SeekOrigin.Current: + real = _position + d; + break; + default: + throw new ArgumentException("Unknown SeekOrigin value", nameof(origin)); + } + + long virt = real - _offset; + if (virt < 0 || virt > Length) + { + throw new ArgumentException("Invalid position", nameof(d)); + } + + _position = _stream.Seek(real, SeekOrigin.Begin); + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _end - _offset; + + public override long Position + { + get => _position - _offset; + set + { + if (value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _position = Seek(value, SeekOrigin.Begin); + } + } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs new file mode 100644 index 0000000000..a8142aef68 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -0,0 +1,681 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Primitives; + +namespace Emby.Server.Implementations.SocketSharp +{ + public partial class WebSocketSharpRequest : IHttpRequest + { + internal static string GetParameter(string header, string attr) + { + int ap = header.IndexOf(attr, StringComparison.Ordinal); + if (ap == -1) + { + return null; + } + + ap += attr.Length; + if (ap >= header.Length) + { + return null; + } + + char ending = header[ap]; + if (ending != '"') + { + ending = ' '; + } + + int end = header.IndexOf(ending, ap + 1); + if (end == -1) + { + return ending == '"' ? null : header.Substring(ap); + } + + return header.Substring(ap + 1, end - ap - 1); + } + + private async Task LoadMultiPart(WebROCollection form) + { + string boundary = GetParameter(ContentType, "; boundary="); + if (boundary == null) + { + return; + } + + using (var requestStream = InputStream) + { + // DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request + // Not ending with \r\n? + var ms = new MemoryStream(32 * 1024); + await requestStream.CopyToAsync(ms).ConfigureAwait(false); + + var input = ms; + ms.WriteByte((byte)'\r'); + ms.WriteByte((byte)'\n'); + + input.Position = 0; + + // Uncomment to debug + // var content = new StreamReader(ms).ReadToEnd(); + // Console.WriteLine(boundary + "::" + content); + // input.Position = 0; + + var multi_part = new HttpMultipart(input, boundary, ContentEncoding); + + HttpMultipart.Element e; + while ((e = multi_part.ReadNextElement()) != null) + { + if (e.Filename == null) + { + byte[] copy = new byte[e.Length]; + + input.Position = e.Start; + input.Read(copy, 0, (int)e.Length); + + form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); + } + else + { + // We use a substream, as in 2.x we will support large uploads streamed to disk, + var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); + files[e.Name] = sub; + } + } + } + } + + public async Task GetFormData() + { + var form = new WebROCollection(); + files = new Dictionary(); + + if (IsContentType("multipart/form-data", true)) + { + await LoadMultiPart(form).ConfigureAwait(false); + } + else if (IsContentType("application/x-www-form-urlencoded", true)) + { + await LoadWwwForm(form).ConfigureAwait(false); + } + +#if NET_4_0 + if (validateRequestNewMode && !checked_form) { + // Setting this before calling the validator prevents + // possible endless recursion + checked_form = true; + ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form); + } else +#endif + if (validate_form && !checked_form) + { + checked_form = true; + ValidateNameValueCollection("Form", form); + } + + return form; + } + + public string Accept => StringValues.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"].ToString(); + + public string Authorization => StringValues.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"].ToString(); + + protected bool validate_cookies { get; set; } + protected bool validate_query_string { get; set; } + protected bool validate_form { get; set; } + protected bool checked_cookies { get; set; } + protected bool checked_query_string { get; set; } + protected bool checked_form { get; set; } + + private static void ThrowValidationException(string name, string key, string value) + { + string v = "\"" + value + "\""; + if (v.Length > 20) + { + v = v.Substring(0, 16) + "...\""; + } + + string msg = string.Format( + CultureInfo.InvariantCulture, + "A potentially dangerous Request.{0} value was detected from the client ({1}={2}).", + name, + key, + v); + + throw new Exception(msg); + } + + private static void ValidateNameValueCollection(string name, QueryParamCollection coll) + { + if (coll == null) + { + return; + } + + foreach (var pair in coll) + { + var key = pair.Name; + var val = pair.Value; + if (val != null && val.Length > 0 && IsInvalidString(val)) + { + ThrowValidationException(name, key, val); + } + } + } + + internal static bool IsInvalidString(string val) + => IsInvalidString(val, out var validationFailureIndex); + + internal static bool IsInvalidString(string val, out int validationFailureIndex) + { + validationFailureIndex = 0; + + int len = val.Length; + if (len < 2) + { + return false; + } + + char current = val[0]; + for (int idx = 1; idx < len; idx++) + { + char next = val[idx]; + + // See http://secunia.com/advisories/14325 + if (current == '<' || current == '\xff1c') + { + if (next == '!' || next < ' ' + || (next >= 'a' && next <= 'z') + || (next >= 'A' && next <= 'Z')) + { + validationFailureIndex = idx - 1; + return true; + } + } + else if (current == '&' && next == '#') + { + validationFailureIndex = idx - 1; + return true; + } + + current = next; + } + + return false; + } + + public void ValidateInput() + { + validate_cookies = true; + validate_query_string = true; + validate_form = true; + } + + private bool IsContentType(string ct, bool starts_with) + { + if (ct == null || ContentType == null) + { + return false; + } + + if (starts_with) + { + return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); + } + + return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); + } + + private async Task LoadWwwForm(WebROCollection form) + { + using (var input = InputStream) + { + using (var ms = new MemoryStream()) + { + await input.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + + using (var s = new StreamReader(ms, ContentEncoding)) + { + var key = new StringBuilder(); + var value = new StringBuilder(); + int c; + + while ((c = s.Read()) != -1) + { + if (c == '=') + { + value.Length = 0; + while ((c = s.Read()) != -1) + { + if (c == '&') + { + AddRawKeyValue(form, key, value); + break; + } + else + { + value.Append((char)c); + } + } + + if (c == -1) + { + AddRawKeyValue(form, key, value); + return; + } + } + else if (c == '&') + { + AddRawKeyValue(form, key, value); + } + else + { + key.Append((char)c); + } + } + + if (c == -1) + { + AddRawKeyValue(form, key, value); + } + } + } + } + } + + private static void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value) + { + form.Add(WebUtility.UrlDecode(key.ToString()), WebUtility.UrlDecode(value.ToString())); + + key.Length = 0; + value.Length = 0; + } + + private Dictionary files; + + private class WebROCollection : QueryParamCollection + { + public override string ToString() + { + var result = new StringBuilder(); + foreach (var pair in this) + { + if (result.Length > 0) + { + result.Append('&'); + } + + var key = pair.Name; + if (key != null && key.Length > 0) + { + result.Append(key); + result.Append('='); + } + + result.Append(pair.Value); + } + + return result.ToString(); + } + } + private class HttpMultipart + { + + public class Element + { + public string ContentType { get; set; } + + public string Name { get; set; } + + public string Filename { get; set; } + + public Encoding Encoding { get; set; } + + public long Start { get; set; } + + public long Length { get; set; } + + public override string ToString() + { + return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + + Start.ToString(CultureInfo.CurrentCulture) + ", Length " + Length.ToString(CultureInfo.CurrentCulture); + } + } + + private const byte LF = (byte)'\n'; + + private const byte CR = (byte)'\r'; + + private Stream data; + + private string boundary; + + private byte[] boundaryBytes; + + private byte[] buffer; + + private bool atEof; + + private Encoding encoding; + + private StringBuilder sb; + + // See RFC 2046 + // In the case of multipart entities, in which one or more different + // sets of data are combined in a single body, a "multipart" media type + // field must appear in the entity's header. The body must then contain + // one or more body parts, each preceded by a boundary delimiter line, + // and the last one followed by a closing boundary delimiter line. + // After its boundary delimiter line, each body part then consists of a + // header area, a blank line, and a body area. Thus a body part is + // similar to an RFC 822 message in syntax, but different in meaning. + + public HttpMultipart(Stream data, string b, Encoding encoding) + { + this.data = data; + boundary = b; + boundaryBytes = encoding.GetBytes(b); + buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--' + this.encoding = encoding; + sb = new StringBuilder(); + } + + public Element ReadNextElement() + { + if (atEof || ReadBoundary()) + { + return null; + } + + var elem = new Element(); + string header; + while ((header = ReadHeaders()) != null) + { + if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) + { + elem.Name = GetContentDispositionAttribute(header, "name"); + elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); + } + else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase)) + { + elem.ContentType = header.Substring("Content-Type:".Length).Trim(); + elem.Encoding = GetEncoding(elem.ContentType); + } + } + + long start = data.Position; + elem.Start = start; + long pos = MoveToNextBoundary(); + if (pos == -1) + { + return null; + } + + elem.Length = pos - start; + return elem; + } + + private string ReadLine() + { + // CRLF or LF are ok as line endings. + bool got_cr = false; + int b = 0; + sb.Length = 0; + while (true) + { + b = data.ReadByte(); + if (b == -1) + { + return null; + } + + if (b == LF) + { + break; + } + + got_cr = b == CR; + sb.Append((char)b); + } + + if (got_cr) + { + sb.Length--; + } + + return sb.ToString(); + } + + private static string GetContentDispositionAttribute(string l, string name) + { + int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); + if (idx < 0) + { + return null; + } + + int begin = idx + name.Length + "=\"".Length; + int end = l.IndexOf('"', begin); + if (end < 0) + { + return null; + } + + if (begin == end) + { + return string.Empty; + } + + return l.Substring(begin, end - begin); + } + + private string GetContentDispositionAttributeWithEncoding(string l, string name) + { + int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); + if (idx < 0) + { + return null; + } + + int begin = idx + name.Length + "=\"".Length; + int end = l.IndexOf('"', begin); + if (end < 0) + { + return null; + } + + if (begin == end) + { + return string.Empty; + } + + string temp = l.Substring(begin, end - begin); + byte[] source = new byte[temp.Length]; + for (int i = temp.Length - 1; i >= 0; i--) + { + source[i] = (byte)temp[i]; + } + + return encoding.GetString(source, 0, source.Length); + } + + private bool ReadBoundary() + { + try + { + string line; + do + { + line = ReadLine(); + } + while (line.Length == 0); + + if (line[0] != '-' || line[1] != '-') + { + return false; + } + + if (!line.EndsWith(boundary, StringComparison.Ordinal)) + { + return true; + } + } + catch + { + + } + + return false; + } + + private string ReadHeaders() + { + string s = ReadLine(); + if (s.Length == 0) + { + return null; + } + + return s; + } + + private static bool CompareBytes(byte[] orig, byte[] other) + { + for (int i = orig.Length - 1; i >= 0; i--) + { + if (orig[i] != other[i]) + { + return false; + } + } + + return true; + } + + private long MoveToNextBoundary() + { + long retval = 0; + bool got_cr = false; + + int state = 0; + int c = data.ReadByte(); + while (true) + { + if (c == -1) + { + return -1; + } + + if (state == 0 && c == LF) + { + retval = data.Position - 1; + if (got_cr) + { + retval--; + } + + state = 1; + c = data.ReadByte(); + } + else if (state == 0) + { + got_cr = c == CR; + c = data.ReadByte(); + } + else if (state == 1 && c == '-') + { + c = data.ReadByte(); + if (c == -1) + { + return -1; + } + + if (c != '-') + { + state = 0; + got_cr = false; + continue; // no ReadByte() here + } + + int nread = data.Read(buffer, 0, buffer.Length); + int bl = buffer.Length; + if (nread != bl) + { + return -1; + } + + if (!CompareBytes(boundaryBytes, buffer)) + { + state = 0; + data.Position = retval + 2; + if (got_cr) + { + data.Position++; + got_cr = false; + } + + c = data.ReadByte(); + continue; + } + + if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') + { + atEof = true; + } + else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) + { + state = 0; + data.Position = retval + 2; + if (got_cr) + { + data.Position++; + got_cr = false; + } + + c = data.ReadByte(); + continue; + } + + data.Position = retval + 2; + if (got_cr) + { + data.Position++; + } + + break; + } + else + { + // state == 1 + state = 0; // no ReadByte() here + } + } + + return retval; + } + + private static string StripPath(string path) + { + if (path == null || path.Length == 0) + { + return path; + } + + if (path.IndexOf(":\\", StringComparison.Ordinal) != 1 + && !path.StartsWith("\\\\", StringComparison.Ordinal)) + { + return path; + } + + return path.Substring(path.LastIndexOf('\\') + 1); + } + } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs new file mode 100644 index 0000000000..80ef451dc7 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -0,0 +1,149 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Net; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.SocketSharp +{ + public class SharpWebSocket : IWebSocket + { + /// + /// The logger + /// + private readonly ILogger _logger; + + public event EventHandler Closed; + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private SocketHttpListener.WebSocket WebSocket { get; set; } + + private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed = false; + + public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) + { + if (socket == null) + { + throw new ArgumentNullException(nameof(socket)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + WebSocket = socket; + + socket.OnMessage += OnSocketMessage; + socket.OnClose += OnSocketClose; + socket.OnError += OnSocketError; + } + + public Task ConnectAsServerAsync() + => WebSocket.ConnectAsServer(); + + public Task StartReceive() + { + return _taskCompletionSource.Task; + } + + private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e) + { + _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty); + + // Closed?.Invoke(this, EventArgs.Empty); + } + + private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e) + { + _taskCompletionSource.TrySetResult(true); + + Closed?.Invoke(this, EventArgs.Empty); + } + + private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e) + { + if (OnReceiveBytes != null) + { + OnReceiveBytes(e.RawData); + } + } + + /// + /// Gets or sets the state. + /// + /// The state. + public WebSocketState State => WebSocket.ReadyState; + + /// + /// Sends the async. + /// + /// The bytes. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) + { + return WebSocket.SendAsync(bytes); + } + + /// + /// Sends the asynchronous. + /// + /// The text. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) + { + return WebSocket.SendAsync(text); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (_disposed) + { + return; + } + + if (dispose) + { + WebSocket.OnMessage -= OnSocketMessage; + WebSocket.OnClose -= OnSocketClose; + WebSocket.OnError -= OnSocketError; + + _cancellationTokenSource.Cancel(); + + WebSocket.CloseAsync().GetAwaiter().GetResult(); + } + + _disposed = true; + } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + public Action OnReceiveBytes { get; set; } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs new file mode 100644 index 0000000000..ab7ddeca20 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; + using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.Net; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + + namespace Emby.Server.Implementations.SocketSharp +{ + public class WebSocketSharpListener : IHttpListener + { + private HttpListener _listener; + + private readonly ILogger _logger; + + private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); + private CancellationToken _disposeCancellationToken; + + public WebSocketSharpListener( + ILogger logger) + { + _logger = logger; + + _disposeCancellationToken = _disposeCancellationTokenSource.Token; + } + + public Func ErrorHandler { get; set; } + public Func RequestHandler { get; set; } + + public Action WebSocketConnecting { get; set; } + + public Action WebSocketConnected { get; set; } + +// public void Start(IEnumerable urlPrefixes) +// { +// // TODO +// //if (_listener == null) +// //{ +// // _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); +// //} +// +// //_listener.EnableDualMode = _enableDualMode; +// +// //if (_certificate != null) +// //{ +// // _listener.LoadCert(_certificate); +// //} +// +// //_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); +// //_listener.Prefixes.AddRange(urlPrefixes); +// +// //_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); +// +// //_listener.Start(); +// +// if (_listener == null) +// { +// _listener = new HttpListener(); +// } +// +// _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); +// +// //foreach (var urlPrefix in urlPrefixes) +// //{ +// // _listener.Prefixes.Add(urlPrefix); +// //} +// _listener.Prefixes.Add("http://localhost:8096/"); +// +// _listener.Start(); +// +// // TODO how to do this in netcore? +// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), +// null); +// } + + private static void LogRequest(ILogger logger, HttpListenerRequest request) + { + var url = request.Url.ToString(); + + logger.LogInformation( + "{0} {1}. UserAgent: {2}", + request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, + url, + request.UserAgent ?? string.Empty); + } +// +// private Task InitTask(IAsyncResult asyncResult, CancellationToken cancellationToken) +// { +// var context = _listener.EndGetContext(asyncResult); +// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), null); +// IHttpRequest httpReq = null; +// var request = context.Request; +// +// try +// { +// if (request.IsWebSocketRequest) +// { +// LogRequest(_logger, request); +// +// return ProcessWebSocketRequest(context); +// } +// +// httpReq = GetRequest(context); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Error processing request"); +// +// httpReq = httpReq ?? GetRequest(context); +// return ErrorHandler(ex, httpReq, true, true); +// } +// +// var uri = request.Url; +// +// return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); +// } + + private async Task ProcessWebSocketRequest(HttpListenerContext ctx) + { + try + { + var endpoint = ctx.Request.RemoteEndPoint.ToString(); + var url = ctx.Request.RawUrl; + + var queryString = new QueryParamCollection(ctx.Request.QueryString); + + var connectingArgs = new WebSocketConnectingEventArgs + { + Url = url, + QueryString = queryString, + Endpoint = endpoint + }; + + WebSocketConnecting?.Invoke(connectingArgs); + + if (connectingArgs.AllowConnection) + { + _logger.LogDebug("Web socket connection allowed"); + + var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); + + if (WebSocketConnected != null) + { + SharpWebSocket socket = null; //new SharpWebSocket(webSocketContext.WebSocket, _logger); + await socket.ConnectAsServerAsync().ConfigureAwait(false); + + WebSocketConnected(new WebSocketConnectEventArgs + { + Url = url, + QueryString = queryString, + WebSocket = socket, + Endpoint = endpoint + }); + + await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); + } + } + else + { + _logger.LogWarning("Web socket connection not allowed"); + ctx.Response.StatusCode = 401; + ctx.Response.Close(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "AcceptWebSocketAsync error"); + ctx.Response.StatusCode = 500; + ctx.Response.Close(); + } + } + + private async Task ReceiveWebSocketAsync(HttpListenerContext ctx, SharpWebSocket socket) + { + try + { + await socket.StartReceive().ConfigureAwait(false); + } + finally + { + TryClose(ctx, 200); + } + } + + private void TryClose(HttpListenerContext ctx, int statusCode) + { + try + { + ctx.Response.StatusCode = statusCode; + ctx.Response.Close(); + } + catch (ObjectDisposedException) + { + // TODO: Investigate and properly fix. + } + catch (Exception ex) + { + _logger.LogError(ex, "Error closing web socket response"); + } + } + + private IHttpRequest GetRequest(HttpRequest httpContext) + { + var urlSegments = httpContext.Path; + + var operationName = urlSegments; + + var req = new WebSocketSharpRequest(httpContext, httpContext.HttpContext.Response, operationName, _logger); + + return req; + } + + public void Start(IEnumerable urlPrefixes) + { + throw new NotImplementedException(); + } + + public Task Stop() + { + _disposeCancellationTokenSource.Cancel(); + _listener?.Close(); + + return Task.CompletedTask; + } + + /// + /// Releases the unmanaged resources and disposes of the managed resources used. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool _disposed; + + /// + /// Releases the unmanaged resources and disposes of the managed resources used. + /// + /// Whether or not the managed resources should be disposed + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + Stop().GetAwaiter().GetResult(); + } + + _disposed = true; + } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs new file mode 100644 index 0000000000..facc544461 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -0,0 +1,539 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using SocketHttpListener.Net; +using IHttpFile = MediaBrowser.Model.Services.IHttpFile; +using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; +using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; +using IResponse = MediaBrowser.Model.Services.IResponse; + +namespace Emby.Server.Implementations.SocketSharp +{ + public partial class WebSocketSharpRequest : IHttpRequest + { + private readonly HttpRequest request; + private readonly IHttpResponse response; + + public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger) + { + this.OperationName = operationName; + this.request = httpContext; + this.response = new WebSocketSharpResponse(logger, response, this); + + // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); + } + + public HttpRequest HttpRequest => request; + + public object OriginalRequest => request; + + public IResponse Response => response; + + public IHttpResponse HttpResponse => response; + + public string OperationName { get; set; } + + public object Dto { get; set; } + + public string RawUrl => request.Path.ToUriComponent(); + + public string AbsoluteUri => request.Path.ToUriComponent().TrimEnd('/'); + + public string UserHostAddress => ""; + + public string XForwardedFor + => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); + + public int? XForwardedPort + => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); + + public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString(); + + public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString(); + + private string remoteIp; + + public string RemoteIp => + remoteIp ?? + (remoteIp = CheckBadChars(XForwardedFor) ?? + NormalizeIp(CheckBadChars(XRealIp) ?? + (string.IsNullOrEmpty(request.Host.Host) ? null : NormalizeIp(request.Host.Host)))); + + private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; + + // CheckBadChars - throws on invalid chars to be not found in header name/value + internal static string CheckBadChars(string name) + { + if (name == null || name.Length == 0) + { + return name; + } + + // VALUE check + // Trim spaces from both ends + name = name.Trim(HttpTrimCharacters); + + // First, check for correctly formed multi-line value + // Second, check for absence of CTL characters + int crlf = 0; + for (int i = 0; i < name.Length; ++i) + { + char c = (char)(0x000000ff & (uint)name[i]); + switch (crlf) + { + case 0: + { + if (c == '\r') + { + crlf = 1; + } + else if (c == '\n') + { + // Technically this is bad HTTP. But it would be a breaking change to throw here. + // Is there an exploit? + crlf = 2; + } + else if (c == 127 || (c < ' ' && c != '\t')) + { + throw new ArgumentException("net_WebHeaderInvalidControlChars"); + } + + break; + } + + case 1: + { + if (c == '\n') + { + crlf = 2; + break; + } + + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + } + + case 2: + { + if (c == ' ' || c == '\t') + { + crlf = 0; + break; + } + + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + } + } + } + + if (crlf != 0) + { + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + } + + return name; + } + + internal static bool ContainsNonAsciiChars(string token) + { + for (int i = 0; i < token.Length; ++i) + { + if ((token[i] < 0x20) || (token[i] > 0x7e)) + { + return true; + } + } + + return false; + } + + private string NormalizeIp(string ip) + { + if (!string.IsNullOrWhiteSpace(ip)) + { + // Handle ipv4 mapped to ipv6 + const string srch = "::ffff:"; + var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); + if (index == 0) + { + ip = ip.Substring(srch.Length); + } + } + + return ip; + } + + public bool IsSecureConnection => request.IsHttps || XForwardedProtocol == "https"; + + public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + + private Dictionary items; + public Dictionary Items => items ?? (items = new Dictionary()); + + private string responseContentType; + public string ResponseContentType + { + get => + responseContentType + ?? (responseContentType = GetResponseContentType(HttpRequest)); + set => this.responseContentType = value; + } + + public const string FormUrlEncoded = "application/x-www-form-urlencoded"; + public const string MultiPartFormData = "multipart/form-data"; + public static string GetResponseContentType(HttpRequest httpReq) + { + var specifiedContentType = GetQueryStringContentType(httpReq); + if (!string.IsNullOrEmpty(specifiedContentType)) + { + return specifiedContentType; + } + + const string serverDefaultContentType = "application/json"; + + var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); // TODO; + string defaultContentType = null; + if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) + { + defaultContentType = serverDefaultContentType; + } + + var acceptsAnything = false; + var hasDefaultContentType = defaultContentType != null; + if (acceptContentTypes != null) + { + foreach (var acceptsType in acceptContentTypes) + { + // TODO: @bond move to Span when Span.Split lands + // https://github.com/dotnet/corefx/issues/26528 + var contentType = acceptsType?.Split(';')[0].Trim(); + acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); + + if (acceptsAnything) + { + break; + } + } + + if (acceptsAnything) + { + if (hasDefaultContentType) + { + return defaultContentType; + } + else + { + return serverDefaultContentType; + } + } + } + + if (acceptContentTypes == null && httpReq.ContentType == Soap11) + { + return Soap11; + } + + // We could also send a '406 Not Acceptable', but this is allowed also + return serverDefaultContentType; + } + + public const string Soap11 = "text/xml; charset=utf-8"; + + public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) + { + if (contentTypes == null || request.ContentType == null) + { + return false; + } + + foreach (var contentType in contentTypes) + { + if (IsContentType(request, contentType)) + { + return true; + } + } + + return false; + } + + public static bool IsContentType(HttpRequest request, string contentType) + { + return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); + } + + private static string GetQueryStringContentType(HttpRequest httpReq) + { + string format = httpReq.Query["format"]; + if (format == null) + { + const int formatMaxLength = 4; + string pi = httpReq.Path.ToString(); + if (pi == null || pi.Length <= formatMaxLength) + { + return null; + } + + if (pi[0] == '/') + { + pi = pi.Substring(1); + } + + format = LeftPart(pi, '/'); + if (format.Length > formatMaxLength) + { + return null; + } + } + + format = LeftPart(format, '.'); + if (format.ToLower().Contains("json")) + { + return "application/json"; + } + else if (format.ToLower().Contains("xml")) + { + return "application/xml"; + } + + return null; + } + + public static string LeftPart(string strVal, char needle) + { + if (strVal == null) + { + return null; + } + + var pos = strVal.IndexOf(needle.ToString(), StringComparison.Ordinal); + return pos == -1 ? strVal : strVal.Substring(0, pos); + } + + public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle) + { + if (strVal == null) + { + return null; + } + + var pos = strVal.IndexOf(needle.ToString()); + return pos == -1 ? strVal : strVal.Slice(0, pos); + } + + public static string HandlerFactoryPath; + + private string pathInfo; + public string PathInfo + { + get + { + if (this.pathInfo == null) + { + var mode = HandlerFactoryPath; + + var pos = request.Path.ToString().IndexOf("?", StringComparison.Ordinal); + if (pos != -1) + { + var path = request.Path.ToString().Substring(0, pos); + this.pathInfo = GetPathInfo( + path, + mode, + mode ?? string.Empty); + } + else + { + this.pathInfo = request.Path.ToString(); + } + + this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); + this.pathInfo = NormalizePathInfo(pathInfo, mode); + } + + return this.pathInfo; + } + } + + private static string GetPathInfo(string fullPath, string mode, string appPath) + { + var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); + if (!string.IsNullOrEmpty(pathInfo)) + { + return pathInfo; + } + + // Wildcard mode relies on this to work out the handlerPath + pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); + if (!string.IsNullOrEmpty(pathInfo)) + { + return pathInfo; + } + + return fullPath; + } + + private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) + { + if (mappedPathRoot == null) + { + return null; + } + + var sbPathInfo = new StringBuilder(); + var fullPathParts = fullPath.Split('/'); + var mappedPathRootParts = mappedPathRoot.Split('/'); + var fullPathIndexOffset = mappedPathRootParts.Length - 1; + var pathRootFound = false; + + for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) + { + if (pathRootFound) + { + sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); + } + else if (fullPathIndex - fullPathIndexOffset >= 0) + { + pathRootFound = true; + for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) + { + if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) + { + pathRootFound = false; + break; + } + } + } + } + + if (!pathRootFound) + { + return null; + } + + var path = sbPathInfo.ToString(); + return path.Length > 1 ? path.TrimEnd('/') : "/"; + } + + private Dictionary cookies; + public IDictionary Cookies + { + get + { + if (cookies == null) + { + cookies = new Dictionary(); + foreach (var cookie in this.request.Cookies) + { + var httpCookie = cookie; + cookies[httpCookie.Key] = new Cookie(httpCookie.Key, httpCookie.Value, "", ""); + } + } + + return cookies; + } + } + + public string UserAgent => request.Headers[HeaderNames.UserAgent]; + + public QueryParamCollection Headers => new QueryParamCollection(request.Headers); + + private QueryParamCollection queryString; + public QueryParamCollection QueryString => queryString ?? (queryString = new QueryParamCollection(request.Query)); + + public bool IsLocal => true; // TODO + + private string httpMethod; + public string HttpMethod => + httpMethod + ?? (httpMethod = request.Method); + + public string Verb => HttpMethod; + + public string ContentType => request.ContentType; + + private Encoding contentEncoding; + public Encoding ContentEncoding + { + get => contentEncoding ?? Encoding.GetEncoding(request.Headers[HeaderNames.ContentEncoding].ToString()); + set => contentEncoding = value; + } + + public Uri UrlReferrer => request.GetTypedHeaders().Referer; + + public static Encoding GetEncoding(string contentTypeHeader) + { + var param = GetParameter(contentTypeHeader, "charset="); + if (param == null) + { + return null; + } + + try + { + return Encoding.GetEncoding(param); + } + catch (ArgumentException) + { + return null; + } + } + + public Stream InputStream => request.Body; + + public long ContentLength => request.ContentLength ?? 0; + + private IHttpFile[] httpFiles; + public IHttpFile[] Files + { + get + { + if (httpFiles == null) + { + if (files == null) + { + return httpFiles = Array.Empty(); + } + + httpFiles = new IHttpFile[files.Count]; + var i = 0; + foreach (var pair in files) + { + var reqFile = pair.Value; + httpFiles[i] = new HttpFile + { + ContentType = reqFile.ContentType, + ContentLength = reqFile.ContentLength, + FileName = reqFile.FileName, + InputStream = reqFile.InputStream, + }; + i++; + } + } + + return httpFiles; + } + } + + public static string NormalizePathInfo(string pathInfo, string handlerPath) + { + if (handlerPath != null) + { + var trimmed = pathInfo.TrimStart('/'); + if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase)) + { + return trimmed.Substring(handlerPath.Length); + } + } + + return pathInfo; + } + } +} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs new file mode 100644 index 0000000000..c68b959846 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; +using IRequest = MediaBrowser.Model.Services.IRequest; + +namespace Emby.Server.Implementations.SocketSharp +{ + public class WebSocketSharpResponse : IHttpResponse + { + private readonly ILogger _logger; + + private readonly HttpResponse _response; + + public WebSocketSharpResponse(ILogger logger, HttpResponse response, IRequest request) + { + _logger = logger; + this._response = response; + Items = new Dictionary(); + Request = request; + } + + public IRequest Request { get; private set; } + + public Dictionary Items { get; private set; } + + public object OriginalResponse => _response; + + public int StatusCode + { + get => this._response.StatusCode; + set => this._response.StatusCode = value; + } + + public string StatusDescription { get; set; } + + public string ContentType + { + get => _response.ContentType; + set => _response.ContentType = value; + } + + public QueryParamCollection Headers => new QueryParamCollection(_response.Headers); + + private static string AsHeaderValue(Cookie cookie) + { + DateTime defaultExpires = DateTime.MinValue; + + var path = cookie.Expires == defaultExpires + ? "/" + : cookie.Path ?? "/"; + + var sb = new StringBuilder(); + + sb.Append($"{cookie.Name}={cookie.Value};path={path}"); + + if (cookie.Expires != defaultExpires) + { + sb.Append($";expires={cookie.Expires:R}"); + } + + if (!string.IsNullOrEmpty(cookie.Domain)) + { + sb.Append($";domain={cookie.Domain}"); + } + + if (cookie.Secure) + { + sb.Append(";Secure"); + } + + if (cookie.HttpOnly) + { + sb.Append(";HttpOnly"); + } + + return sb.ToString(); + } + + public void AddHeader(string name, string value) + { + if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + ContentType = value; + return; + } + + _response.Headers.Add(name, value); + } + + public string GetHeader(string name) + { + return _response.Headers[name]; + } + + public void Redirect(string url) + { + _response.Redirect(url); + } + + public Stream OutputStream => _response.Body; + + public void Close() + { + if (!this.IsClosed) + { + this.IsClosed = true; + + try + { + var response = this._response; + + var outputStream = response.Body; + + // This is needed with compression + outputStream.Flush(); + outputStream.Dispose(); + } + catch (SocketException) + { + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in HttpListenerResponseWrapper"); + } + } + } + + public bool IsClosed + { + get; + private set; + } + + public void SetContentLength(long contentLength) + { + // you can happily set the Content-Length header in Asp.Net + // but HttpListener will complain if you do - you have to set ContentLength64 on the response. + // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header + //_response.ContentLength64 = contentLength; + } + + public void SetCookie(Cookie cookie) + { + var cookieStr = AsHeaderValue(cookie); + _response.Headers.Add("Set-Cookie", cookieStr); + } + + public bool SendChunked { get; set; } + + public bool KeepAlive { get; set; } + + public void ClearCookies() + { + } + const int StreamCopyToBufferSize = 81920; + public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) + { + // TODO + // return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); + var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + //if (count <= 0) + //{ + // allowAsync = true; + //} + + var fileOpenOptions = FileOpenOptions.SequentialScan; + + if (allowAsync) + { + fileOpenOptions |= FileOpenOptions.Asynchronous; + } + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + + using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + { + if (offset > 0) + { + fs.Position = offset; + } + + if (count > 0) + { + await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/Emby.Server.Implementations/Startup.cs b/Emby.Server.Implementations/Startup.cs new file mode 100644 index 0000000000..164c7eeaa7 --- /dev/null +++ b/Emby.Server.Implementations/Startup.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using MediaBrowser.Api; +using MediaBrowser.Controller; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) => Configuration = configuration; + + // Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + // Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + + } + } + +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 41ee73a565..bfc50468f7 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -20,6 +20,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -143,7 +144,7 @@ namespace Jellyfin.Server appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); await appHost.RunStartupTasks().ConfigureAwait(false); - + appHost.Host.Run(); // TODO: read input for a stop command try diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index f17fd7159d..2f1ea13d9b 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors @@ -13,6 +13,8 @@ + + diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 0e0ebf848b..09806ed9c2 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.Linq; using System.Net; using MediaBrowser.Model.Dto; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services { @@ -23,6 +24,14 @@ namespace MediaBrowser.Model.Services } } + public QueryParamCollection(Microsoft.AspNetCore.Http.IHeaderDictionary headers) + { + foreach (var pair in headers) + { + Add(pair.Key, pair.Value); + } + } + // TODO remove this shit public QueryParamCollection(WebHeaderCollection webHeaderCollection) { @@ -47,6 +56,14 @@ namespace MediaBrowser.Model.Services } } + public QueryParamCollection(IQueryCollection queryCollection) + { + foreach (var pair in queryCollection) + { + Add(pair.Key, pair.Value); + } + } + private static StringComparison GetStringComparison() { return StringComparison.OrdinalIgnoreCase; From c3fa299acc1612966ee0d778c3e86f71c21a252a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 07:22:49 +0100 Subject: [PATCH 0074/6208] Remove hardcoded path and fix url bug in Windows --- Emby.Server.Implementations/ApplicationHost.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5e8611d531..56089320ef 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -109,6 +109,7 @@ using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -621,8 +622,8 @@ namespace Emby.Server.Implementations Host = new WebHostBuilder() .UseKestrel() - .UseContentRoot("/Users/clausvium/RiderProjects/jellyfin/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin-web/src") - .UseStartup() + .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), @"jellyfin-web\src")) + //.UseStartup() // .ConfigureServices(async services => // { // services.AddSingleton(startUp); @@ -726,7 +727,7 @@ namespace Emby.Server.Implementations } var req = new WebSocketSharpRequest(request, response, request.Path, Logger); - await ((HttpListenerHost)HttpServer).RequestHandler(req,request.Path.ToString(), request.Host.ToString(), data.Values["localpath"].ToString(), CancellationToken.None).ConfigureAwait(false); + await ((HttpListenerHost)HttpServer).RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), data.Values["localpath"].ToString(), CancellationToken.None).ConfigureAwait(false); } protected virtual IHttpClient CreateHttpClient() From f3e7bc0573e69df236ac8c565a354520dd094775 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 08:09:42 +0100 Subject: [PATCH 0075/6208] Replace some todos with http extensions and prepare some socket work --- .../ApplicationHost.cs | 110 ++++++++++-------- .../HttpServer/HttpListenerHost.cs | 7 ++ .../SocketSharp/WebSocketSharpListener.cs | 42 +++---- .../SocketSharp/WebSocketSharpRequest.cs | 13 ++- 4 files changed, 94 insertions(+), 78 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 56089320ef..18e752e53b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -110,10 +110,12 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using ServiceStack; using HttpResponse = MediaBrowser.Model.Net.HttpResponse; using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; @@ -642,7 +644,12 @@ namespace Emby.Server.Implementations // await RunStartupTasks().ConfigureAwait(false); // }) .UseUrls("http://localhost:8096") - .ConfigureServices(s => s.AddRouting()) + .ConfigureServices(services => + { + services.AddRouting(); + services.AddHttpContextAccessor(); + services.TryAddSingleton(); + }) .Configure( app => { app.UseWebSockets(new WebSocketOptions { @@ -668,62 +675,63 @@ namespace Emby.Server.Implementations var ctx = request.HttpContext; if (ctx.WebSockets.IsWebSocketRequest) { - try - { - var endpoint = ctx.Request.Path.ToString(); - var url = ctx.Request.Path.ToString(); + await ((HttpListenerHost)HttpServer)._websocketlistener.ProcessWebSocketRequest(ctx).ConfigureAwait(false); +// try +// { +// var endpoint = ctx.Request.Path.ToString(); +// var url = ctx.Request.Path.ToString(); - var queryString = new QueryParamCollection(request.Query); + // var queryString = new QueryParamCollection(request.Query); - var connectingArgs = new WebSocketConnectingEventArgs - { - Url = url, - QueryString = queryString, - Endpoint = endpoint - }; + // var connectingArgs = new WebSocketConnectingEventArgs + // { + // Url = url, + // QueryString = queryString, + // Endpoint = endpoint + // }; - if (connectingArgs.AllowConnection) - { - Logger.LogDebug("Web socket connection allowed"); + // if (connectingArgs.AllowConnection) + // { + // Logger.LogDebug("Web socket connection allowed"); - var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; + // var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; - //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); - //socket.ConnectAsServerAsync().ConfigureAwait(false); + // //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); + // //socket.ConnectAsServerAsync().ConfigureAwait(false); -// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) -// { -// OnReceive = ProcessWebSocketMessageReceived, -// Url = e.Url, -// QueryString = e.QueryString ?? new QueryParamCollection() -// }; -// -// connection.Closed += Connection_Closed; -// -// lock (_webSocketConnections) -// { -// _webSocketConnections.Add(connection); -// } -// -// WebSocketConnected(new WebSocketConnectEventArgs -// { -// Url = url, -// QueryString = queryString, -// WebSocket = socket, -// Endpoint = endpoint -// }); - await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); - } - else - { - Logger.LogWarning("Web socket connection not allowed"); - ctx.Response.StatusCode = 401; - } - } - catch (Exception ex) - { - ctx.Response.StatusCode = 500; - } + //// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) + //// { + //// OnReceive = ProcessWebSocketMessageReceived, + //// Url = e.Url, + //// QueryString = e.QueryString ?? new QueryParamCollection() + //// }; + //// + //// connection.Closed += Connection_Closed; + //// + //// lock (_webSocketConnections) + //// { + //// _webSocketConnections.Add(connection); + //// } + //// + //// WebSocketConnected(new WebSocketConnectEventArgs + //// { + //// Url = url, + //// QueryString = queryString, + //// WebSocket = socket, + //// Endpoint = endpoint + //// }); + // await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); + // } + // else + // { + // Logger.LogWarning("Web socket connection not allowed"); + // ctx.Response.StatusCode = 401; + // } + // } + // catch (Exception ex) + // { + // ctx.Response.StatusCode = 500; + // } } var req = new WebSocketSharpRequest(request, response, request.Path, Logger); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 1bd084259d..cfe0bbe974 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; +using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -73,6 +74,10 @@ namespace Emby.Server.Implementations.HttpServer Instance = this; ResponseFilters = Array.Empty>(); + _websocketlistener = new WebSocketSharpListener(_logger) + { + WebSocketConnected = OnWebSocketConnected + }; } public string GlobalResponse { get; set; } @@ -796,6 +801,8 @@ namespace Emby.Server.Implementations.HttpServer private bool _disposed; private readonly object _disposeLock = new object(); + public WebSocketSharpListener _websocketlistener; + protected virtual void Dispose(bool disposing) { if (_disposed) return; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index ab7ddeca20..824c9a822d 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -8,6 +8,7 @@ using Emby.Server.Implementations.Net; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.SocketSharp @@ -120,14 +121,14 @@ using Microsoft.Extensions.Logging; // return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); // } - private async Task ProcessWebSocketRequest(HttpListenerContext ctx) + public async Task ProcessWebSocketRequest(HttpContext ctx) { try { - var endpoint = ctx.Request.RemoteEndPoint.ToString(); - var url = ctx.Request.RawUrl; + var endpoint = ctx.Connection.RemoteIpAddress.ToString(); + var url = ctx.Request.GetDisplayUrl(); - var queryString = new QueryParamCollection(ctx.Request.QueryString); + var queryString = new QueryParamCollection(ctx.Request.Query); var connectingArgs = new WebSocketConnectingEventArgs { @@ -142,40 +143,40 @@ using Microsoft.Extensions.Logging; { _logger.LogDebug("Web socket connection allowed"); - var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); + var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); if (WebSocketConnected != null) { - SharpWebSocket socket = null; //new SharpWebSocket(webSocketContext.WebSocket, _logger); - await socket.ConnectAsServerAsync().ConfigureAwait(false); + //SharpWebSocket socket = new SharpWebSocket(webSocketContext, _logger); + //await socket.ConnectAsServerAsync().ConfigureAwait(false); - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = queryString, - WebSocket = socket, - Endpoint = endpoint - }); + //WebSocketConnected(new WebSocketConnectEventArgs + //{ + // Url = url, + // QueryString = queryString, + // WebSocket = socket, + // Endpoint = endpoint + //}); - await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); + //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); } } else { _logger.LogWarning("Web socket connection not allowed"); ctx.Response.StatusCode = 401; - ctx.Response.Close(); + //ctx.Response.Close(); } } catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); ctx.Response.StatusCode = 500; - ctx.Response.Close(); + //ctx.Response.Close(); } } - private async Task ReceiveWebSocketAsync(HttpListenerContext ctx, SharpWebSocket socket) + private async Task ReceiveWebSocketAsync(HttpContext ctx, SharpWebSocket socket) { try { @@ -187,12 +188,11 @@ using Microsoft.Extensions.Logging; } } - private void TryClose(HttpListenerContext ctx, int statusCode) + private void TryClose(HttpContext ctx, int statusCode) { try { ctx.Response.StatusCode = statusCode; - ctx.Response.Close(); } catch (ObjectDisposedException) { diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index facc544461..9c5b2b0834 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -7,6 +7,7 @@ using System.Text; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -44,11 +45,11 @@ namespace Emby.Server.Implementations.SocketSharp public object Dto { get; set; } - public string RawUrl => request.Path.ToUriComponent(); + public string RawUrl => request.Path.ToString(); - public string AbsoluteUri => request.Path.ToUriComponent().TrimEnd('/'); + public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); - public string UserHostAddress => ""; + public string UserHostAddress => request.HttpContext.Connection.RemoteIpAddress.ToString(); public string XForwardedFor => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); @@ -66,7 +67,7 @@ namespace Emby.Server.Implementations.SocketSharp remoteIp ?? (remoteIp = CheckBadChars(XForwardedFor) ?? NormalizeIp(CheckBadChars(XRealIp) ?? - (string.IsNullOrEmpty(request.Host.Host) ? null : NormalizeIp(request.Host.Host)))); + (string.IsNullOrEmpty(request.HttpContext.Connection.RemoteIpAddress.ToString()) ? null : NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString())))); private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; @@ -199,7 +200,7 @@ namespace Emby.Server.Implementations.SocketSharp const string serverDefaultContentType = "application/json"; - var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); // TODO; + var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); string defaultContentType = null; if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) { @@ -448,7 +449,7 @@ namespace Emby.Server.Implementations.SocketSharp private QueryParamCollection queryString; public QueryParamCollection QueryString => queryString ?? (queryString = new QueryParamCollection(request.Query)); - public bool IsLocal => true; // TODO + public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString()); private string httpMethod; public string HttpMethod => From 194da8416bb93b492b86bab6dd199eb5dcc06e55 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 10:23:58 +0100 Subject: [PATCH 0076/6208] Use middlewares instead of Routing --- .../ApplicationHost.cs | 147 +++++++++--------- .../SocketSharp/SharpWebSocket.cs | 14 +- 2 files changed, 79 insertions(+), 82 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 18e752e53b..ca1dceedfa 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -657,85 +657,92 @@ namespace Emby.Server.Implementations ReceiveBufferSize = 0x10000 }); - app.UseRouter(r => - { - // TODO all the verbs, but really MVC... - r.MapGet("/{*localpath}", ExecuteHandler); - r.MapPut("/{*localpath}", ExecuteHandler); - r.MapPost("/{*localpath}", ExecuteHandler); - r.MapDelete("/{*localpath}", ExecuteHandler); - r.MapVerb("HEAD", "/{*localpath}", ExecuteHandler); - }); + app.Use(ExecuteWebsocketHandlerAsync); + app.Use(ExecuteHttpHandlerAsync); }) .Build(); } - public async Task ExecuteHandler(HttpRequest request, Microsoft.AspNetCore.Http.HttpResponse response, RouteData data) + public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) { - var ctx = request.HttpContext; - if (ctx.WebSockets.IsWebSocketRequest) + if (!context.WebSockets.IsWebSocketRequest) { - await ((HttpListenerHost)HttpServer)._websocketlistener.ProcessWebSocketRequest(ctx).ConfigureAwait(false); -// try -// { -// var endpoint = ctx.Request.Path.ToString(); -// var url = ctx.Request.Path.ToString(); - - // var queryString = new QueryParamCollection(request.Query); - - // var connectingArgs = new WebSocketConnectingEventArgs - // { - // Url = url, - // QueryString = queryString, - // Endpoint = endpoint - // }; - - // if (connectingArgs.AllowConnection) - // { - // Logger.LogDebug("Web socket connection allowed"); - - // var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; - - // //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); - // //socket.ConnectAsServerAsync().ConfigureAwait(false); - - //// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) - //// { - //// OnReceive = ProcessWebSocketMessageReceived, - //// Url = e.Url, - //// QueryString = e.QueryString ?? new QueryParamCollection() - //// }; - //// - //// connection.Closed += Connection_Closed; - //// - //// lock (_webSocketConnections) - //// { - //// _webSocketConnections.Add(connection); - //// } - //// - //// WebSocketConnected(new WebSocketConnectEventArgs - //// { - //// Url = url, - //// QueryString = queryString, - //// WebSocket = socket, - //// Endpoint = endpoint - //// }); - // await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); - // } - // else - // { - // Logger.LogWarning("Web socket connection not allowed"); - // ctx.Response.StatusCode = 401; - // } - // } - // catch (Exception ex) - // { - // ctx.Response.StatusCode = 500; - // } + await next(); + return; } + await ((HttpListenerHost)HttpServer)._websocketlistener.ProcessWebSocketRequest(context).ConfigureAwait(false); + // try + // { + // var endpoint = ctx.Request.Path.ToString(); + // var url = ctx.Request.Path.ToString(); + + // var queryString = new QueryParamCollection(request.Query); + + // var connectingArgs = new WebSocketConnectingEventArgs + // { + // Url = url, + // QueryString = queryString, + // Endpoint = endpoint + // }; + + // if (connectingArgs.AllowConnection) + // { + // Logger.LogDebug("Web socket connection allowed"); + + // var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; + + // //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); + // //socket.ConnectAsServerAsync().ConfigureAwait(false); + + //// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) + //// { + //// OnReceive = ProcessWebSocketMessageReceived, + //// Url = e.Url, + //// QueryString = e.QueryString ?? new QueryParamCollection() + //// }; + //// + //// connection.Closed += Connection_Closed; + //// + //// lock (_webSocketConnections) + //// { + //// _webSocketConnections.Add(connection); + //// } + //// + //// WebSocketConnected(new WebSocketConnectEventArgs + //// { + //// Url = url, + //// QueryString = queryString, + //// WebSocket = socket, + //// Endpoint = endpoint + //// }); + // await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); + // } + // else + // { + // Logger.LogWarning("Web socket connection not allowed"); + // ctx.Response.StatusCode = 401; + // } + // } + // catch (Exception ex) + // { + // ctx.Response.StatusCode = 500; + // } + } + public async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) + { + if (context.WebSockets.IsWebSocketRequest) + { + await next(); + return; + } + + var request = context.Request; + var response = context.Response; + var localPath = context.Request.Path.ToString().TrimStart('/'); + var req = new WebSocketSharpRequest(request, response, request.Path, Logger); - await ((HttpListenerHost)HttpServer).RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), data.Values["localpath"].ToString(), CancellationToken.None).ConfigureAwait(false); + await ((HttpListenerHost)HttpServer).RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); } protected virtual IHttpClient CreateHttpClient() diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index 80ef451dc7..854d33f3a0 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -28,18 +28,8 @@ namespace Emby.Server.Implementations.SocketSharp public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - _logger = logger; - WebSocket = socket; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + WebSocket = socket ?? throw new ArgumentNullException(nameof(socket)); socket.OnMessage += OnSocketMessage; socket.OnClose += OnSocketClose; From 38f52a139e8017ddaaef55e1eb4cc97f1d4d8c04 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 10:30:51 +0100 Subject: [PATCH 0077/6208] Add response compression middleware --- Emby.Server.Implementations/ApplicationHost.cs | 3 ++- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ca1dceedfa..da9550b851 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -646,7 +646,7 @@ namespace Emby.Server.Implementations .UseUrls("http://localhost:8096") .ConfigureServices(services => { - services.AddRouting(); + services.AddResponseCompression(); services.AddHttpContextAccessor(); services.TryAddSingleton(); }) @@ -657,6 +657,7 @@ namespace Emby.Server.Implementations ReceiveBufferSize = 0x10000 }); + app.UseResponseCompression(); app.Use(ExecuteWebsocketHandlerAsync); app.Use(ExecuteHttpHandlerAsync); }) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index acbc60c39e..a4dac4a7f9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,6 +29,8 @@ + + From d6c6f3c10cff68b11df884be502c58a17da0d332 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 15:13:06 +0100 Subject: [PATCH 0078/6208] Still broken --- .../ApplicationHost.cs | 2 +- .../Net/WebSocketConnectEventArgs.cs | 1 + .../SocketSharp/SharpWebSocket.cs | 31 ++++++------- .../SocketSharp/WebSocketSharpListener.cs | 44 ++++++++++++++----- .../SocketSharp/WebSocketSharpListener.cs | 4 +- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index da9550b851..65b6681c89 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -740,7 +740,7 @@ namespace Emby.Server.Implementations var request = context.Request; var response = context.Response; - var localPath = context.Request.Path.ToString().TrimStart('/'); + var localPath = context.Request.Path.ToString(); var req = new WebSocketSharpRequest(request, response, request.Path, Logger); await ((HttpListenerHost)HttpServer).RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs index 3ab8e854a0..666f1f6011 100644 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Net.WebSockets; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.Net diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index 854d33f3a0..89004ba7fe 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -1,5 +1,6 @@ using System; using System.Net.WebSockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; @@ -20,25 +21,18 @@ namespace Emby.Server.Implementations.SocketSharp /// Gets or sets the web socket. ///
/// The web socket. - private SocketHttpListener.WebSocket WebSocket { get; set; } + private WebSocket WebSocket { get; set; } private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private bool _disposed = false; - public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) + public SharpWebSocket(WebSocket socket, ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); WebSocket = socket ?? throw new ArgumentNullException(nameof(socket)); - - socket.OnMessage += OnSocketMessage; - socket.OnClose += OnSocketClose; - socket.OnError += OnSocketError; } - public Task ConnectAsServerAsync() - => WebSocket.ConnectAsServer(); - public Task StartReceive() { return _taskCompletionSource.Task; @@ -58,7 +52,7 @@ namespace Emby.Server.Implementations.SocketSharp Closed?.Invoke(this, EventArgs.Empty); } - private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e) + private void OnSocketMessage(SocketHttpListener.MessageEventArgs e) { if (OnReceiveBytes != null) { @@ -66,11 +60,15 @@ namespace Emby.Server.Implementations.SocketSharp } } + public Task ConnectAsServerAsync() + { + return Task.CompletedTask; + } /// /// Gets or sets the state. /// /// The state. - public WebSocketState State => WebSocket.ReadyState; + public WebSocketState State => WebSocket.State; /// /// Sends the async. @@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.SocketSharp /// Task. public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(bytes); + return WebSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); } /// @@ -93,7 +91,7 @@ namespace Emby.Server.Implementations.SocketSharp /// Task. public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(text); + return WebSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); } /// @@ -118,13 +116,10 @@ namespace Emby.Server.Implementations.SocketSharp if (dispose) { - WebSocket.OnMessage -= OnSocketMessage; - WebSocket.OnClose -= OnSocketClose; - WebSocket.OnError -= OnSocketError; - _cancellationTokenSource.Cancel(); - WebSocket.CloseAsync().GetAwaiter().GetResult(); + // TODO + WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult(); } _disposed = true; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 824c9a822d..b64fc06833 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; @@ -144,22 +145,41 @@ using Microsoft.Extensions.Logging; _logger.LogDebug("Web socket connection allowed"); var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); + var socket = new SharpWebSocket(webSocketContext, _logger); + await socket.ConnectAsServerAsync().ConfigureAwait(false); - if (WebSocketConnected != null) + WebSocketConnected(new WebSocketConnectEventArgs { - //SharpWebSocket socket = new SharpWebSocket(webSocketContext, _logger); - //await socket.ConnectAsServerAsync().ConfigureAwait(false); + Url = url, + QueryString = queryString, + WebSocket = socket, + Endpoint = endpoint + }); - //WebSocketConnected(new WebSocketConnectEventArgs - //{ - // Url = url, - // QueryString = queryString, - // WebSocket = socket, - // Endpoint = endpoint - //}); + //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); + var buffer = WebSocket.CreateClientBuffer(1024 * 4, 1024 * 4); + WebSocketReceiveResult result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); + socket.OnReceiveBytes(buffer.Array); + //while (!result.CloseStatus.HasValue) + //{ + // await webSocketContext.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); - } + // result = await webSocketContext.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + //} + //WebSocketConnected?.Invoke(new WebSocketConnectEventArgs + //{ + // Url = url, + // QueryString = queryString, + // WebSocket = webSocketContext, + // Endpoint = endpoint + //}); + await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + //SharpWebSocket socket = new SharpWebSocket(webSocketContext, _logger); + //await socket.ConnectAsServerAsync().ConfigureAwait(false); + + + + //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); } else { diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs index c458fdb78f..4dc4103cb1 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -152,7 +152,7 @@ using Microsoft.Extensions.Logging; { Url = url, QueryString = queryString, - WebSocket = socket, + WebSocket = null, // socket, Endpoint = endpoint }); From 5a7cca9d1bdc5280626a6654cdbd8c0d45016af5 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 19:48:18 +0100 Subject: [PATCH 0079/6208] Fix websockets and RawUrl --- .../SocketSharp/SharpWebSocket.cs | 2 +- .../SocketSharp/WebSocketSharpListener.cs | 13 +++++++++++-- .../SocketSharp/WebSocketSharpRequest.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index 89004ba7fe..eab903db8e 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.SocketSharp _cancellationTokenSource.Cancel(); // TODO - WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult(); + // WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult(); } _disposed = true; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index b64fc06833..05f159b4ee 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -160,6 +160,15 @@ using Microsoft.Extensions.Logging; var buffer = WebSocket.CreateClientBuffer(1024 * 4, 1024 * 4); WebSocketReceiveResult result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); socket.OnReceiveBytes(buffer.Array); + + while (result.MessageType != WebSocketMessageType.Close) + { + result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); + socket.OnReceiveBytes(buffer.Array); + } + await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + socket.Dispose(); + //while (!result.CloseStatus.HasValue) //{ // await webSocketContext.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); @@ -173,11 +182,11 @@ using Microsoft.Extensions.Logging; // WebSocket = webSocketContext, // Endpoint = endpoint //}); - await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + //await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); //SharpWebSocket socket = new SharpWebSocket(webSocketContext, _logger); //await socket.ConnectAsServerAsync().ConfigureAwait(false); - + //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 9c5b2b0834..b07c4bfebb 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.SocketSharp public object Dto { get; set; } - public string RawUrl => request.Path.ToString(); + public string RawUrl => request.GetEncodedPathAndQuery(); public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); From a85488cd205532662c59ada5032dc81c0315543b Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 20:13:48 +0100 Subject: [PATCH 0080/6208] Fix websockets array index out of bounds and some cleanup --- .../ApplicationHost.cs | 78 +------- .../HttpServer/HttpListenerHost.cs | 7 +- .../SocketSharp/WebSocketSharpListener.cs | 166 ++---------------- 3 files changed, 24 insertions(+), 227 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 65b6681c89..e9d8d8de7e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -624,25 +624,7 @@ namespace Emby.Server.Implementations Host = new WebHostBuilder() .UseKestrel() - .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), @"jellyfin-web\src")) - //.UseStartup() -// .ConfigureServices(async services => -// { -// services.AddSingleton(startUp); -// RegisterResources(services); -// FindParts(); -// try -// { -// ImageProcessor.ImageEncoder = -// new NullImageEncoder(); //SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager); -// } -// catch (Exception ex) -// { -// Logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}"); -// ImageProcessor.ImageEncoder = new NullImageEncoder(); -// } -// await RunStartupTasks().ConfigureAwait(false); -// }) + .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "jellyfin-web", "src")) .UseUrls("http://localhost:8096") .ConfigureServices(services => { @@ -672,63 +654,7 @@ namespace Emby.Server.Implementations return; } - await ((HttpListenerHost)HttpServer)._websocketlistener.ProcessWebSocketRequest(context).ConfigureAwait(false); - // try - // { - // var endpoint = ctx.Request.Path.ToString(); - // var url = ctx.Request.Path.ToString(); - - // var queryString = new QueryParamCollection(request.Query); - - // var connectingArgs = new WebSocketConnectingEventArgs - // { - // Url = url, - // QueryString = queryString, - // Endpoint = endpoint - // }; - - // if (connectingArgs.AllowConnection) - // { - // Logger.LogDebug("Web socket connection allowed"); - - // var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result; - - // //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger); - // //socket.ConnectAsServerAsync().ConfigureAwait(false); - - //// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger) - //// { - //// OnReceive = ProcessWebSocketMessageReceived, - //// Url = e.Url, - //// QueryString = e.QueryString ?? new QueryParamCollection() - //// }; - //// - //// connection.Closed += Connection_Closed; - //// - //// lock (_webSocketConnections) - //// { - //// _webSocketConnections.Add(connection); - //// } - //// - //// WebSocketConnected(new WebSocketConnectEventArgs - //// { - //// Url = url, - //// QueryString = queryString, - //// WebSocket = socket, - //// Endpoint = endpoint - //// }); - // await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false); - // } - // else - // { - // Logger.LogWarning("Web socket connection not allowed"); - // ctx.Response.StatusCode = 401; - // } - // } - // catch (Exception ex) - // { - // ctx.Response.StatusCode = 500; - // } + await ((HttpListenerHost)HttpServer).ProcessWebSocketRequest(context).ConfigureAwait(false); } public async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index cfe0bbe974..a3eaa2fb50 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -21,6 +21,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ServiceStack.Text.Jsv; @@ -767,6 +768,10 @@ namespace Emby.Server.Implementations.HttpServer return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } + public Task ProcessWebSocketRequest(HttpContext context) + { + return _websocketlistener.ProcessWebSocketRequest(context); + } //TODO Add Jellyfin Route Path Normalizer private static string NormalizeEmbyRoutePath(string path) @@ -801,7 +806,7 @@ namespace Emby.Server.Implementations.HttpServer private bool _disposed; private readonly object _disposeLock = new object(); - public WebSocketSharpListener _websocketlistener; + private readonly WebSocketSharpListener _websocketlistener; protected virtual void Dispose(bool disposing) { diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 05f159b4ee..6e5190efdb 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; - using System.Net; +using System.Linq; +using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -38,94 +39,18 @@ using Microsoft.Extensions.Logging; public Action WebSocketConnected { get; set; } -// public void Start(IEnumerable urlPrefixes) -// { -// // TODO -// //if (_listener == null) -// //{ -// // _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); -// //} -// -// //_listener.EnableDualMode = _enableDualMode; -// -// //if (_certificate != null) -// //{ -// // _listener.LoadCert(_certificate); -// //} -// -// //_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); -// //_listener.Prefixes.AddRange(urlPrefixes); -// -// //_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); -// -// //_listener.Start(); -// -// if (_listener == null) -// { -// _listener = new HttpListener(); -// } -// -// _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); -// -// //foreach (var urlPrefix in urlPrefixes) -// //{ -// // _listener.Prefixes.Add(urlPrefix); -// //} -// _listener.Prefixes.Add("http://localhost:8096/"); -// -// _listener.Start(); -// -// // TODO how to do this in netcore? -// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), -// null); -// } - - private static void LogRequest(ILogger logger, HttpListenerRequest request) + private static void LogRequest(ILogger logger, HttpRequest request) { - var url = request.Url.ToString(); + var url = request.GetDisplayUrl(); - logger.LogInformation( - "{0} {1}. UserAgent: {2}", - request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, - url, - request.UserAgent ?? string.Empty); + logger.LogInformation("{0} {1}. UserAgent: {2}", "WS", url, request.Headers["User-Agent"].ToString()); } -// -// private Task InitTask(IAsyncResult asyncResult, CancellationToken cancellationToken) -// { -// var context = _listener.EndGetContext(asyncResult); -// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), null); -// IHttpRequest httpReq = null; -// var request = context.Request; -// -// try -// { -// if (request.IsWebSocketRequest) -// { -// LogRequest(_logger, request); -// -// return ProcessWebSocketRequest(context); -// } -// -// httpReq = GetRequest(context); -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error processing request"); -// -// httpReq = httpReq ?? GetRequest(context); -// return ErrorHandler(ex, httpReq, true, true); -// } -// -// var uri = request.Url; -// -// return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); -// } public async Task ProcessWebSocketRequest(HttpContext ctx) { try { + LogRequest(_logger, ctx.Request); var endpoint = ctx.Connection.RemoteIpAddress.ToString(); var url = ctx.Request.GetDisplayUrl(); @@ -156,94 +81,35 @@ using Microsoft.Extensions.Logging; Endpoint = endpoint }); - //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); - var buffer = WebSocket.CreateClientBuffer(1024 * 4, 1024 * 4); - WebSocketReceiveResult result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); - socket.OnReceiveBytes(buffer.Array); + var buffer = WebSocket.CreateClientBuffer(4096, 4096); + WebSocketReceiveResult result; + var message = new List(); - while (result.MessageType != WebSocketMessageType.Close) + do { result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); socket.OnReceiveBytes(buffer.Array); - } - await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + message.AddRange(buffer.Array.Take(result.Count)); + } while (!result.EndOfMessage && result.MessageType != WebSocketMessageType.Close); + + socket.OnReceiveBytes(message.ToArray()); + await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, CancellationToken.None); socket.Dispose(); - - //while (!result.CloseStatus.HasValue) - //{ - // await webSocketContext.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - // result = await webSocketContext.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - //} - //WebSocketConnected?.Invoke(new WebSocketConnectEventArgs - //{ - // Url = url, - // QueryString = queryString, - // WebSocket = webSocketContext, - // Endpoint = endpoint - //}); - //await webSocketContext.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - //SharpWebSocket socket = new SharpWebSocket(webSocketContext, _logger); - //await socket.ConnectAsServerAsync().ConfigureAwait(false); - - - - //await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false); } else { _logger.LogWarning("Web socket connection not allowed"); ctx.Response.StatusCode = 401; - //ctx.Response.Close(); } } catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); ctx.Response.StatusCode = 500; - //ctx.Response.Close(); } } - private async Task ReceiveWebSocketAsync(HttpContext ctx, SharpWebSocket socket) - { - try - { - await socket.StartReceive().ConfigureAwait(false); - } - finally - { - TryClose(ctx, 200); - } - } - - private void TryClose(HttpContext ctx, int statusCode) - { - try - { - ctx.Response.StatusCode = statusCode; - } - catch (ObjectDisposedException) - { - // TODO: Investigate and properly fix. - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing web socket response"); - } - } - - private IHttpRequest GetRequest(HttpRequest httpContext) - { - var urlSegments = httpContext.Path; - - var operationName = urlSegments; - - var req = new WebSocketSharpRequest(httpContext, httpContext.HttpContext.Response, operationName, _logger); - - return req; - } - public void Start(IEnumerable urlPrefixes) { throw new NotImplementedException(); From 4e8de67acabbfe1aeb5fee6d1162abc760fbd540 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 20:22:40 +0100 Subject: [PATCH 0081/6208] Remove SocketSharp from Jellyfin.Server and some other cleanup --- .../ApplicationHost.cs | 45 +- .../HttpServer/HttpListenerHost.cs | 15 +- Jellyfin.Server/CoreAppHost.cs | 4 - Jellyfin.Server/Jellyfin.Server.csproj | 2 +- Jellyfin.Server/SocketSharp/HttpFile.cs | 18 - Jellyfin.Server/SocketSharp/HttpPostedFile.cs | 204 ------ Jellyfin.Server/SocketSharp/RequestMono.cs | 680 ------------------ Jellyfin.Server/SocketSharp/SharpWebSocket.cs | 149 ---- .../SocketSharp/WebSocketSharpListener.cs | 237 ------ .../SocketSharp/WebSocketSharpRequest.cs | 535 -------------- .../SocketSharp/WebSocketSharpResponse.cs | 215 ------ 11 files changed, 11 insertions(+), 2093 deletions(-) delete mode 100644 Jellyfin.Server/SocketSharp/HttpFile.cs delete mode 100644 Jellyfin.Server/SocketSharp/HttpPostedFile.cs delete mode 100644 Jellyfin.Server/SocketSharp/RequestMono.cs delete mode 100644 Jellyfin.Server/SocketSharp/SharpWebSocket.cs delete mode 100644 Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs delete mode 100644 Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs delete mode 100644 Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e9d8d8de7e..ad6b8e84a8 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -791,7 +791,8 @@ namespace Emby.Server.Implementations _configuration, NetworkManager, JsonSerializer, - XmlSerializer); + XmlSerializer, + CreateHttpListener()); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); serviceCollection.AddSingleton(HttpServer); @@ -1125,8 +1126,6 @@ namespace Emby.Server.Implementations HttpServer.Init(GetExports(false), GetExports()); - //StartServer(); - LibraryManager.AddParts(GetExports(), GetExports(), GetExports(), @@ -1239,45 +1238,7 @@ namespace Emby.Server.Implementations }); } - protected abstract IHttpListener CreateHttpListener(); - - /// - /// Starts the server. - /// - private void StartServer() - { - try - { - ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); - return; - } - catch (Exception ex) - { - var msg = string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase) - ? "The http server is unable to start due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system." - : "Error starting Http Server"; - - Logger.LogError(ex, msg); - - if (HttpPort == ServerConfiguration.DefaultHttpPort) - { - throw; - } - } - - HttpPort = ServerConfiguration.DefaultHttpPort; - - try - { - ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error starting http server"); - - throw; - } - } + protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger); private CertificateInfo GetCertificateInfo(bool generateCertificate) { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index a3eaa2fb50..12c23a8693 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -44,6 +44,7 @@ namespace Emby.Server.Implementations.HttpServer private readonly IServerApplicationHost _appHost; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; + private readonly IHttpListener _socketListener; private readonly Func> _funcParseFn; public Action[] ResponseFilters { get; set; } @@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.HttpServer IConfiguration configuration, INetworkManager networkManager, IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer) + IXmlSerializer xmlSerializer, + IHttpListener socketListener) { _appHost = applicationHost; _logger = loggerFactory.CreateLogger("HttpServer"); @@ -70,15 +72,13 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; + _socketListener = socketListener; + _socketListener.WebSocketConnected = OnWebSocketConnected; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); Instance = this; ResponseFilters = Array.Empty>(); - _websocketlistener = new WebSocketSharpListener(_logger) - { - WebSocketConnected = OnWebSocketConnected - }; } public string GlobalResponse { get; set; } @@ -770,7 +770,8 @@ namespace Emby.Server.Implementations.HttpServer public Task ProcessWebSocketRequest(HttpContext context) { - return _websocketlistener.ProcessWebSocketRequest(context); + // TODO + return ((WebSocketSharpListener)_socketListener).ProcessWebSocketRequest(context); } //TODO Add Jellyfin Route Path Normalizer @@ -867,8 +868,6 @@ namespace Emby.Server.Implementations.HttpServer _listener.WebSocketConnected = OnWebSocketConnected; _listener.ErrorHandler = ErrorHandler; _listener.RequestHandler = RequestHandler; - - _listener.Start(UrlPrefixes); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 68fd2453e3..9e52247900 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Reflection; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; -using Jellyfin.Server.SocketSharp; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; @@ -45,8 +44,5 @@ namespace Jellyfin.Server } protected override void ShutdownInternal() => Program.Shutdown(); - - protected override IHttpListener CreateHttpListener() - => new WebSocketSharpListener(Logger); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bd670df527..94a7e58cb5 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,4 +1,4 @@ - + jellyfin diff --git a/Jellyfin.Server/SocketSharp/HttpFile.cs b/Jellyfin.Server/SocketSharp/HttpFile.cs deleted file mode 100644 index 448b666b63..0000000000 --- a/Jellyfin.Server/SocketSharp/HttpFile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.IO; -using MediaBrowser.Model.Services; - -namespace Jellyfin.Server.SocketSharp -{ - public class HttpFile : IHttpFile - { - public string Name { get; set; } - - public string FileName { get; set; } - - public long ContentLength { get; set; } - - public string ContentType { get; set; } - - public Stream InputStream { get; set; } - } -} diff --git a/Jellyfin.Server/SocketSharp/HttpPostedFile.cs b/Jellyfin.Server/SocketSharp/HttpPostedFile.cs deleted file mode 100644 index f38ed848ee..0000000000 --- a/Jellyfin.Server/SocketSharp/HttpPostedFile.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -public sealed class HttpPostedFile : IDisposable -{ - private string _name; - private string _contentType; - private Stream _stream; - private bool _disposed = false; - - internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) - { - _name = name; - _contentType = content_type; - _stream = new ReadSubStream(base_stream, offset, length); - } - - public string ContentType => _contentType; - - public int ContentLength => (int)_stream.Length; - - public string FileName => _name; - - public Stream InputStream => _stream; - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _stream.Dispose(); - _stream = null; - - _name = null; - _contentType = null; - - _disposed = true; - } - - private class ReadSubStream : Stream - { - private Stream _stream; - private long _offset; - private long _end; - private long _position; - - public ReadSubStream(Stream s, long offset, long length) - { - _stream = s; - _offset = offset; - _end = offset + length; - _position = offset; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int dest_offset, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (dest_offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "< 0"); - } - - int len = buffer.Length; - if (dest_offset > len) - { - throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset)); - } - - // reordered to avoid possible integer overflow - if (dest_offset > len - count) - { - throw new ArgumentException("Reading would overrun buffer", nameof(count)); - } - - if (count > _end - _position) - { - count = (int)(_end - _position); - } - - if (count <= 0) - { - return 0; - } - - _stream.Position = _position; - int result = _stream.Read(buffer, dest_offset, count); - if (result > 0) - { - _position += result; - } - else - { - _position = _end; - } - - return result; - } - - public override int ReadByte() - { - if (_position >= _end) - { - return -1; - } - - _stream.Position = _position; - int result = _stream.ReadByte(); - if (result < 0) - { - _position = _end; - } - else - { - _position++; - } - - return result; - } - - public override long Seek(long d, SeekOrigin origin) - { - long real; - switch (origin) - { - case SeekOrigin.Begin: - real = _offset + d; - break; - case SeekOrigin.End: - real = _end + d; - break; - case SeekOrigin.Current: - real = _position + d; - break; - default: - throw new ArgumentException("Unknown SeekOrigin value", nameof(origin)); - } - - long virt = real - _offset; - if (virt < 0 || virt > Length) - { - throw new ArgumentException("Invalid position", nameof(d)); - } - - _position = _stream.Seek(real, SeekOrigin.Begin); - return _position; - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => _end - _offset; - - public override long Position - { - get => _position - _offset; - set - { - if (value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _position = Seek(value, SeekOrigin.Begin); - } - } - } -} diff --git a/Jellyfin.Server/SocketSharp/RequestMono.cs b/Jellyfin.Server/SocketSharp/RequestMono.cs deleted file mode 100644 index f2a08c9ae2..0000000000 --- a/Jellyfin.Server/SocketSharp/RequestMono.cs +++ /dev/null @@ -1,680 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Jellyfin.Server.SocketSharp -{ - public partial class WebSocketSharpRequest : IHttpRequest - { - internal static string GetParameter(string header, string attr) - { - int ap = header.IndexOf(attr, StringComparison.Ordinal); - if (ap == -1) - { - return null; - } - - ap += attr.Length; - if (ap >= header.Length) - { - return null; - } - - char ending = header[ap]; - if (ending != '"') - { - ending = ' '; - } - - int end = header.IndexOf(ending, ap + 1); - if (end == -1) - { - return ending == '"' ? null : header.Substring(ap); - } - - return header.Substring(ap + 1, end - ap - 1); - } - - private async Task LoadMultiPart(WebROCollection form) - { - string boundary = GetParameter(ContentType, "; boundary="); - if (boundary == null) - { - return; - } - - using (var requestStream = InputStream) - { - // DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request - // Not ending with \r\n? - var ms = new MemoryStream(32 * 1024); - await requestStream.CopyToAsync(ms).ConfigureAwait(false); - - var input = ms; - ms.WriteByte((byte)'\r'); - ms.WriteByte((byte)'\n'); - - input.Position = 0; - - // Uncomment to debug - // var content = new StreamReader(ms).ReadToEnd(); - // Console.WriteLine(boundary + "::" + content); - // input.Position = 0; - - var multi_part = new HttpMultipart(input, boundary, ContentEncoding); - - HttpMultipart.Element e; - while ((e = multi_part.ReadNextElement()) != null) - { - if (e.Filename == null) - { - byte[] copy = new byte[e.Length]; - - input.Position = e.Start; - input.Read(copy, 0, (int)e.Length); - - form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); - } - else - { - // We use a substream, as in 2.x we will support large uploads streamed to disk, - var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files[e.Name] = sub; - } - } - } - } - - public async Task GetFormData() - { - var form = new WebROCollection(); - files = new Dictionary(); - - if (IsContentType("multipart/form-data", true)) - { - await LoadMultiPart(form).ConfigureAwait(false); - } - else if (IsContentType("application/x-www-form-urlencoded", true)) - { - await LoadWwwForm(form).ConfigureAwait(false); - } - -#if NET_4_0 - if (validateRequestNewMode && !checked_form) { - // Setting this before calling the validator prevents - // possible endless recursion - checked_form = true; - ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form); - } else -#endif - if (validate_form && !checked_form) - { - checked_form = true; - ValidateNameValueCollection("Form", form); - } - - return form; - } - - public string Accept => string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"]; - - public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"]; - - protected bool validate_cookies { get; set; } - protected bool validate_query_string { get; set; } - protected bool validate_form { get; set; } - protected bool checked_cookies { get; set; } - protected bool checked_query_string { get; set; } - protected bool checked_form { get; set; } - - private static void ThrowValidationException(string name, string key, string value) - { - string v = "\"" + value + "\""; - if (v.Length > 20) - { - v = v.Substring(0, 16) + "...\""; - } - - string msg = string.Format( - CultureInfo.InvariantCulture, - "A potentially dangerous Request.{0} value was detected from the client ({1}={2}).", - name, - key, - v); - - throw new Exception(msg); - } - - private static void ValidateNameValueCollection(string name, QueryParamCollection coll) - { - if (coll == null) - { - return; - } - - foreach (var pair in coll) - { - var key = pair.Name; - var val = pair.Value; - if (val != null && val.Length > 0 && IsInvalidString(val)) - { - ThrowValidationException(name, key, val); - } - } - } - - internal static bool IsInvalidString(string val) - => IsInvalidString(val, out var validationFailureIndex); - - internal static bool IsInvalidString(string val, out int validationFailureIndex) - { - validationFailureIndex = 0; - - int len = val.Length; - if (len < 2) - { - return false; - } - - char current = val[0]; - for (int idx = 1; idx < len; idx++) - { - char next = val[idx]; - - // See http://secunia.com/advisories/14325 - if (current == '<' || current == '\xff1c') - { - if (next == '!' || next < ' ' - || (next >= 'a' && next <= 'z') - || (next >= 'A' && next <= 'Z')) - { - validationFailureIndex = idx - 1; - return true; - } - } - else if (current == '&' && next == '#') - { - validationFailureIndex = idx - 1; - return true; - } - - current = next; - } - - return false; - } - - public void ValidateInput() - { - validate_cookies = true; - validate_query_string = true; - validate_form = true; - } - - private bool IsContentType(string ct, bool starts_with) - { - if (ct == null || ContentType == null) - { - return false; - } - - if (starts_with) - { - return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); - } - - private async Task LoadWwwForm(WebROCollection form) - { - using (var input = InputStream) - { - using (var ms = new MemoryStream()) - { - await input.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - - using (var s = new StreamReader(ms, ContentEncoding)) - { - var key = new StringBuilder(); - var value = new StringBuilder(); - int c; - - while ((c = s.Read()) != -1) - { - if (c == '=') - { - value.Length = 0; - while ((c = s.Read()) != -1) - { - if (c == '&') - { - AddRawKeyValue(form, key, value); - break; - } - else - { - value.Append((char)c); - } - } - - if (c == -1) - { - AddRawKeyValue(form, key, value); - return; - } - } - else if (c == '&') - { - AddRawKeyValue(form, key, value); - } - else - { - key.Append((char)c); - } - } - - if (c == -1) - { - AddRawKeyValue(form, key, value); - } - } - } - } - } - - private static void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value) - { - form.Add(WebUtility.UrlDecode(key.ToString()), WebUtility.UrlDecode(value.ToString())); - - key.Length = 0; - value.Length = 0; - } - - private Dictionary files; - - private class WebROCollection : QueryParamCollection - { - public override string ToString() - { - var result = new StringBuilder(); - foreach (var pair in this) - { - if (result.Length > 0) - { - result.Append('&'); - } - - var key = pair.Name; - if (key != null && key.Length > 0) - { - result.Append(key); - result.Append('='); - } - - result.Append(pair.Value); - } - - return result.ToString(); - } - } - private class HttpMultipart - { - - public class Element - { - public string ContentType { get; set; } - - public string Name { get; set; } - - public string Filename { get; set; } - - public Encoding Encoding { get; set; } - - public long Start { get; set; } - - public long Length { get; set; } - - public override string ToString() - { - return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + - Start.ToString(CultureInfo.CurrentCulture) + ", Length " + Length.ToString(CultureInfo.CurrentCulture); - } - } - - private const byte LF = (byte)'\n'; - - private const byte CR = (byte)'\r'; - - private Stream data; - - private string boundary; - - private byte[] boundaryBytes; - - private byte[] buffer; - - private bool atEof; - - private Encoding encoding; - - private StringBuilder sb; - - // See RFC 2046 - // In the case of multipart entities, in which one or more different - // sets of data are combined in a single body, a "multipart" media type - // field must appear in the entity's header. The body must then contain - // one or more body parts, each preceded by a boundary delimiter line, - // and the last one followed by a closing boundary delimiter line. - // After its boundary delimiter line, each body part then consists of a - // header area, a blank line, and a body area. Thus a body part is - // similar to an RFC 822 message in syntax, but different in meaning. - - public HttpMultipart(Stream data, string b, Encoding encoding) - { - this.data = data; - boundary = b; - boundaryBytes = encoding.GetBytes(b); - buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--' - this.encoding = encoding; - sb = new StringBuilder(); - } - - public Element ReadNextElement() - { - if (atEof || ReadBoundary()) - { - return null; - } - - var elem = new Element(); - string header; - while ((header = ReadHeaders()) != null) - { - if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) - { - elem.Name = GetContentDispositionAttribute(header, "name"); - elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); - } - else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase)) - { - elem.ContentType = header.Substring("Content-Type:".Length).Trim(); - elem.Encoding = GetEncoding(elem.ContentType); - } - } - - long start = data.Position; - elem.Start = start; - long pos = MoveToNextBoundary(); - if (pos == -1) - { - return null; - } - - elem.Length = pos - start; - return elem; - } - - private string ReadLine() - { - // CRLF or LF are ok as line endings. - bool got_cr = false; - int b = 0; - sb.Length = 0; - while (true) - { - b = data.ReadByte(); - if (b == -1) - { - return null; - } - - if (b == LF) - { - break; - } - - got_cr = b == CR; - sb.Append((char)b); - } - - if (got_cr) - { - sb.Length--; - } - - return sb.ToString(); - } - - private static string GetContentDispositionAttribute(string l, string name) - { - int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); - if (idx < 0) - { - return null; - } - - int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); - if (end < 0) - { - return null; - } - - if (begin == end) - { - return string.Empty; - } - - return l.Substring(begin, end - begin); - } - - private string GetContentDispositionAttributeWithEncoding(string l, string name) - { - int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); - if (idx < 0) - { - return null; - } - - int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); - if (end < 0) - { - return null; - } - - if (begin == end) - { - return string.Empty; - } - - string temp = l.Substring(begin, end - begin); - byte[] source = new byte[temp.Length]; - for (int i = temp.Length - 1; i >= 0; i--) - { - source[i] = (byte)temp[i]; - } - - return encoding.GetString(source, 0, source.Length); - } - - private bool ReadBoundary() - { - try - { - string line; - do - { - line = ReadLine(); - } - while (line.Length == 0); - - if (line[0] != '-' || line[1] != '-') - { - return false; - } - - if (!line.EndsWith(boundary, StringComparison.Ordinal)) - { - return true; - } - } - catch - { - - } - - return false; - } - - private string ReadHeaders() - { - string s = ReadLine(); - if (s.Length == 0) - { - return null; - } - - return s; - } - - private static bool CompareBytes(byte[] orig, byte[] other) - { - for (int i = orig.Length - 1; i >= 0; i--) - { - if (orig[i] != other[i]) - { - return false; - } - } - - return true; - } - - private long MoveToNextBoundary() - { - long retval = 0; - bool got_cr = false; - - int state = 0; - int c = data.ReadByte(); - while (true) - { - if (c == -1) - { - return -1; - } - - if (state == 0 && c == LF) - { - retval = data.Position - 1; - if (got_cr) - { - retval--; - } - - state = 1; - c = data.ReadByte(); - } - else if (state == 0) - { - got_cr = c == CR; - c = data.ReadByte(); - } - else if (state == 1 && c == '-') - { - c = data.ReadByte(); - if (c == -1) - { - return -1; - } - - if (c != '-') - { - state = 0; - got_cr = false; - continue; // no ReadByte() here - } - - int nread = data.Read(buffer, 0, buffer.Length); - int bl = buffer.Length; - if (nread != bl) - { - return -1; - } - - if (!CompareBytes(boundaryBytes, buffer)) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - - c = data.ReadByte(); - continue; - } - - if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') - { - atEof = true; - } - else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - - c = data.ReadByte(); - continue; - } - - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - } - - break; - } - else - { - // state == 1 - state = 0; // no ReadByte() here - } - } - - return retval; - } - - private static string StripPath(string path) - { - if (path == null || path.Length == 0) - { - return path; - } - - if (path.IndexOf(":\\", StringComparison.Ordinal) != 1 - && !path.StartsWith("\\\\", StringComparison.Ordinal)) - { - return path; - } - - return path.Substring(path.LastIndexOf('\\') + 1); - } - } - } -} diff --git a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs b/Jellyfin.Server/SocketSharp/SharpWebSocket.cs deleted file mode 100644 index 9b0951857f..0000000000 --- a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Net; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.SocketSharp -{ - public class SharpWebSocket : IWebSocket - { - /// - /// The logger - /// - private readonly ILogger _logger; - - public event EventHandler Closed; - - /// - /// Gets or sets the web socket. - /// - /// The web socket. - private SocketHttpListener.WebSocket WebSocket { get; set; } - - private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed = false; - - public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) - { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - _logger = logger; - WebSocket = socket; - - socket.OnMessage += OnSocketMessage; - socket.OnClose += OnSocketClose; - socket.OnError += OnSocketError; - } - - public Task ConnectAsServerAsync() - => WebSocket.ConnectAsServer(); - - public Task StartReceive() - { - return _taskCompletionSource.Task; - } - - private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e) - { - _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty); - - // Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e) - { - _taskCompletionSource.TrySetResult(true); - - Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e) - { - if (OnReceiveBytes != null) - { - OnReceiveBytes(e.RawData); - } - } - - /// - /// Gets or sets the state. - /// - /// The state. - public WebSocketState State => WebSocket.ReadyState; - - /// - /// Sends the async. - /// - /// The bytes. - /// if set to true [end of message]. - /// The cancellation token. - /// Task. - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - return WebSocket.SendAsync(bytes); - } - - /// - /// Sends the asynchronous. - /// - /// The text. - /// if set to true [end of message]. - /// The cancellation token. - /// Task. - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - return WebSocket.SendAsync(text); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (_disposed) - { - return; - } - - if (dispose) - { - WebSocket.OnMessage -= OnSocketMessage; - WebSocket.OnClose -= OnSocketClose; - WebSocket.OnError -= OnSocketError; - - _cancellationTokenSource.Cancel(); - - WebSocket.CloseAsync().GetAwaiter().GetResult(); - } - - _disposed = true; - } - - /// - /// Gets or sets the receive action. - /// - /// The receive action. - public Action OnReceiveBytes { get; set; } - } -} diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index 4dc4103cb1..0000000000 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; - using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.Net; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - - namespace Jellyfin.Server.SocketSharp -{ - public class WebSocketSharpListener : IHttpListener - { - private HttpListener _listener; - - private readonly ILogger _logger; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private CancellationToken _disposeCancellationToken; - - public WebSocketSharpListener( - ILogger logger) - { - _logger = logger; - - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Func ErrorHandler { get; set; } - public Func RequestHandler { get; set; } - - public Action WebSocketConnecting { get; set; } - - public Action WebSocketConnected { get; set; } - - public void Start(IEnumerable urlPrefixes) - { - // TODO - //if (_listener == null) - //{ - // _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); - //} - - //_listener.EnableDualMode = _enableDualMode; - - //if (_certificate != null) - //{ - // _listener.LoadCert(_certificate); - //} - - //_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); - //_listener.Prefixes.AddRange(urlPrefixes); - - //_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); - - //_listener.Start(); - - if (_listener == null) - { - _listener = new HttpListener(); - } - - _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); - - //foreach (var urlPrefix in urlPrefixes) - //{ - // _listener.Prefixes.Add(urlPrefix); - //} - _listener.Prefixes.Add("http://localhost:8096/"); - - _listener.Start(); - - // TODO how to do this in netcore? - _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), - null); - } - - private static void LogRequest(ILogger logger, HttpListenerRequest request) - { - var url = request.Url.ToString(); - - logger.LogInformation( - "{0} {1}. UserAgent: {2}", - request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, - url, - request.UserAgent ?? string.Empty); - } - - private Task InitTask(IAsyncResult asyncResult, CancellationToken cancellationToken) - { - var context = _listener.EndGetContext(asyncResult); - _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), null); - IHttpRequest httpReq = null; - var request = context.Request; - - try - { - if (request.IsWebSocketRequest) - { - LogRequest(_logger, request); - - return ProcessWebSocketRequest(context); - } - - httpReq = GetRequest(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing request"); - - httpReq = httpReq ?? GetRequest(context); - return ErrorHandler(ex, httpReq, true, true); - } - - var uri = request.Url; - - return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); - } - - private async Task ProcessWebSocketRequest(HttpListenerContext ctx) - { - try - { - var endpoint = ctx.Request.RemoteEndPoint.ToString(); - var url = ctx.Request.RawUrl; - - var queryString = new QueryParamCollection(ctx.Request.QueryString); - - var connectingArgs = new WebSocketConnectingEventArgs - { - Url = url, - QueryString = queryString, - Endpoint = endpoint - }; - - WebSocketConnecting?.Invoke(connectingArgs); - - if (connectingArgs.AllowConnection) - { - _logger.LogDebug("Web socket connection allowed"); - - var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); - - if (WebSocketConnected != null) - { - SharpWebSocket socket = null; //new SharpWebSocket(webSocketContext.WebSocket, _logger); - await socket.ConnectAsServerAsync().ConfigureAwait(false); - - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = queryString, - WebSocket = null, // socket, - Endpoint = endpoint - }); - - await socket.StartReceive().ConfigureAwait(false); - } - } - else - { - _logger.LogWarning("Web socket connection not allowed"); - TryClose(ctx, 401); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "AcceptWebSocketAsync error"); - TryClose(ctx, 500); - } - } - - private void TryClose(HttpListenerContext ctx, int statusCode) - { - try - { - ctx.Response.StatusCode = statusCode; - ctx.Response.Close(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing web socket response"); - } - } - - private IHttpRequest GetRequest(HttpListenerContext httpContext) - { - var urlSegments = httpContext.Request.Url.Segments; - - var operationName = urlSegments[urlSegments.Length - 1]; - - var req = new WebSocketSharpRequest(httpContext, operationName, _logger); - - return req; - } - - public Task Stop() - { - _disposeCancellationTokenSource.Cancel(); - _listener?.Close(); - - return Task.CompletedTask; - } - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool _disposed; - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - /// Whether or not the managed resources should be disposed - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop().GetAwaiter().GetResult(); - } - - _disposed = true; - } - } -} diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index 6a00e283c6..0000000000 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,535 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using SocketHttpListener.Net; -using IHttpFile = MediaBrowser.Model.Services.IHttpFile; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; -using IResponse = MediaBrowser.Model.Services.IResponse; - -namespace Jellyfin.Server.SocketSharp -{ - public partial class WebSocketSharpRequest : IHttpRequest - { - private readonly HttpListenerRequest request; - private readonly IHttpResponse response; - - public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger) - { - this.OperationName = operationName; - this.request = httpContext.Request; - this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); - - // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); - } - - public HttpListenerRequest HttpRequest => request; - - public object OriginalRequest => request; - - public IResponse Response => response; - - public IHttpResponse HttpResponse => response; - - public string OperationName { get; set; } - - public object Dto { get; set; } - - public string RawUrl => request.RawUrl; - - public string AbsoluteUri => request.Url.AbsoluteUri.TrimEnd('/'); - - public string UserHostAddress => request.UserHostAddress; - - public string XForwardedFor - => string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"]; - - public int? XForwardedPort - => string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); - - public string XForwardedProtocol => string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"]; - - public string XRealIp => string.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; - - private string remoteIp; - public string RemoteIp => - remoteIp ?? - (remoteIp = CheckBadChars(XForwardedFor) ?? - NormalizeIp(CheckBadChars(XRealIp) ?? - (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); - - private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; - - // CheckBadChars - throws on invalid chars to be not found in header name/value - internal static string CheckBadChars(string name) - { - if (name == null || name.Length == 0) - { - return name; - } - - // VALUE check - // Trim spaces from both ends - name = name.Trim(HttpTrimCharacters); - - // First, check for correctly formed multi-line value - // Second, check for absence of CTL characters - int crlf = 0; - for (int i = 0; i < name.Length; ++i) - { - char c = (char)(0x000000ff & (uint)name[i]); - switch (crlf) - { - case 0: - { - if (c == '\r') - { - crlf = 1; - } - else if (c == '\n') - { - // Technically this is bad HTTP. But it would be a breaking change to throw here. - // Is there an exploit? - crlf = 2; - } - else if (c == 127 || (c < ' ' && c != '\t')) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); - } - - break; - } - - case 1: - { - if (c == '\n') - { - crlf = 2; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - } - - case 2: - { - if (c == ' ' || c == '\t') - { - crlf = 0; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - } - } - } - - if (crlf != 0) - { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - } - - return name; - } - - internal static bool ContainsNonAsciiChars(string token) - { - for (int i = 0; i < token.Length; ++i) - { - if ((token[i] < 0x20) || (token[i] > 0x7e)) - { - return true; - } - } - - return false; - } - - private string NormalizeIp(string ip) - { - if (!string.IsNullOrWhiteSpace(ip)) - { - // Handle ipv4 mapped to ipv6 - const string srch = "::ffff:"; - var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - if (index == 0) - { - ip = ip.Substring(srch.Length); - } - } - - return ip; - } - - public bool IsSecureConnection => request.IsSecureConnection || XForwardedProtocol == "https"; - - public string[] AcceptTypes => request.AcceptTypes; - - private Dictionary items; - public Dictionary Items => items ?? (items = new Dictionary()); - - private string responseContentType; - public string ResponseContentType - { - get => - responseContentType - ?? (responseContentType = GetResponseContentType(this)); - set => this.responseContentType = value; - } - - public const string FormUrlEncoded = "application/x-www-form-urlencoded"; - public const string MultiPartFormData = "multipart/form-data"; - public static string GetResponseContentType(IRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) - { - return specifiedContentType; - } - - const string serverDefaultContentType = "application/json"; - - var acceptContentTypes = httpReq.AcceptTypes; - string defaultContentType = null; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = serverDefaultContentType; - } - - var acceptsAnything = false; - var hasDefaultContentType = defaultContentType != null; - if (acceptContentTypes != null) - { - foreach (var acceptsType in acceptContentTypes) - { - // TODO: @bond move to Span when Span.Split lands - // https://github.com/dotnet/corefx/issues/26528 - var contentType = acceptsType?.Split(';')[0].Trim(); - acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); - - if (acceptsAnything) - { - break; - } - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - { - return defaultContentType; - } - else - { - return serverDefaultContentType; - } - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - // We could also send a '406 Not Acceptable', but this is allowed also - return serverDefaultContentType; - } - - public const string Soap11 = "text/xml; charset=utf-8"; - - public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) - { - return false; - } - - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) - { - return true; - } - } - - return false; - } - - public static bool IsContentType(IRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - private static string GetQueryStringContentType(IRequest httpReq) - { - ReadOnlySpan format = httpReq.QueryString["format"]; - if (format == null) - { - const int formatMaxLength = 4; - ReadOnlySpan pi = httpReq.PathInfo; - if (pi == null || pi.Length <= formatMaxLength) - { - return null; - } - - if (pi[0] == '/') - { - pi = pi.Slice(1); - } - - format = LeftPart(pi, '/'); - if (format.Length > formatMaxLength) - { - return null; - } - } - - format = LeftPart(format, '.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) - { - return "application/xml"; - } - - return null; - } - - public static string LeftPart(string strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle, StringComparison.Ordinal); - return pos == -1 ? strVal : strVal.Substring(0, pos); - } - - public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle); - return pos == -1 ? strVal : strVal.Slice(0, pos); - } - - public static string HandlerFactoryPath; - - private string pathInfo; - public string PathInfo - { - get - { - if (this.pathInfo == null) - { - var mode = HandlerFactoryPath; - - var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal); - if (pos != -1) - { - var path = request.RawUrl.Substring(0, pos); - this.pathInfo = GetPathInfo( - path, - mode, - mode ?? string.Empty); - } - else - { - this.pathInfo = request.RawUrl; - } - - this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode); - } - - return this.pathInfo; - } - } - - private static string GetPathInfo(string fullPath, string mode, string appPath) - { - var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - // Wildcard mode relies on this to work out the handlerPath - pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - return fullPath; - } - - private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) - { - if (mappedPathRoot == null) - { - return null; - } - - var sbPathInfo = new StringBuilder(); - var fullPathParts = fullPath.Split('/'); - var mappedPathRootParts = mappedPathRoot.Split('/'); - var fullPathIndexOffset = mappedPathRootParts.Length - 1; - var pathRootFound = false; - - for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) - { - if (pathRootFound) - { - sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); - } - else if (fullPathIndex - fullPathIndexOffset >= 0) - { - pathRootFound = true; - for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) - { - if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) - { - pathRootFound = false; - break; - } - } - } - } - - if (!pathRootFound) - { - return null; - } - - var path = sbPathInfo.ToString(); - return path.Length > 1 ? path.TrimEnd('/') : "/"; - } - - private Dictionary cookies; - public IDictionary Cookies - { - get - { - if (cookies == null) - { - cookies = new Dictionary(); - foreach (var cookie in this.request.Cookies) - { - var httpCookie = (System.Net.Cookie)cookie; - cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain); - } - } - - return cookies; - } - } - - public string UserAgent => request.UserAgent; - - public QueryParamCollection Headers => new QueryParamCollection(request.Headers); - - private QueryParamCollection queryString; - public QueryParamCollection QueryString => queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); - - public bool IsLocal => request.IsLocal; - - private string httpMethod; - public string HttpMethod => - httpMethod - ?? (httpMethod = request.HttpMethod); - - public string Verb => HttpMethod; - - public string ContentType => request.ContentType; - - private Encoding contentEncoding; - public Encoding ContentEncoding - { - get => contentEncoding ?? request.ContentEncoding; - set => contentEncoding = value; - } - - public Uri UrlReferrer => request.UrlReferrer; - - public static Encoding GetEncoding(string contentTypeHeader) - { - var param = GetParameter(contentTypeHeader, "charset="); - if (param == null) - { - return null; - } - - try - { - return Encoding.GetEncoding(param); - } - catch (ArgumentException) - { - return null; - } - } - - public Stream InputStream => request.InputStream; - - public long ContentLength => request.ContentLength64; - - private IHttpFile[] httpFiles; - public IHttpFile[] Files - { - get - { - if (httpFiles == null) - { - if (files == null) - { - return httpFiles = Array.Empty(); - } - - httpFiles = new IHttpFile[files.Count]; - var i = 0; - foreach (var pair in files) - { - var reqFile = pair.Value; - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - i++; - } - } - - return httpFiles; - } - } - - public static string NormalizePathInfo(string pathInfo, string handlerPath) - { - if (handlerPath != null) - { - var trimmed = pathInfo.TrimStart('/'); - if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase)) - { - return trimmed.Substring(handlerPath.Length); - } - } - - return pathInfo; - } - } -} diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs deleted file mode 100644 index 552ae1754d..0000000000 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; -using IRequest = MediaBrowser.Model.Services.IRequest; - -namespace Jellyfin.Server.SocketSharp -{ - public class WebSocketSharpResponse : IHttpResponse - { - private readonly ILogger _logger; - - private readonly HttpListenerResponse _response; - - public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request) - { - _logger = logger; - this._response = response; - Items = new Dictionary(); - Request = request; - } - - public IRequest Request { get; private set; } - - public Dictionary Items { get; private set; } - - public object OriginalResponse => _response; - - public int StatusCode - { - get => this._response.StatusCode; - set => this._response.StatusCode = value; - } - - public string StatusDescription - { - get => this._response.StatusDescription; - set => this._response.StatusDescription = value; - } - - public string ContentType - { - get => _response.ContentType; - set => _response.ContentType = value; - } - - public QueryParamCollection Headers => new QueryParamCollection(_response.Headers); - - private static string AsHeaderValue(Cookie cookie) - { - DateTime defaultExpires = DateTime.MinValue; - - var path = cookie.Expires == defaultExpires - ? "/" - : cookie.Path ?? "/"; - - var sb = new StringBuilder(); - - sb.Append($"{cookie.Name}={cookie.Value};path={path}"); - - if (cookie.Expires != defaultExpires) - { - sb.Append($";expires={cookie.Expires:R}"); - } - - if (!string.IsNullOrEmpty(cookie.Domain)) - { - sb.Append($";domain={cookie.Domain}"); - } - - if (cookie.Secure) - { - sb.Append(";Secure"); - } - - if (cookie.HttpOnly) - { - sb.Append(";HttpOnly"); - } - - return sb.ToString(); - } - - public void AddHeader(string name, string value) - { - if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) - { - ContentType = value; - return; - } - - _response.AddHeader(name, value); - } - - public string GetHeader(string name) - { - return _response.Headers[name]; - } - - public void Redirect(string url) - { - _response.Redirect(url); - } - - public Stream OutputStream => _response.OutputStream; - - public void Close() - { - if (!this.IsClosed) - { - this.IsClosed = true; - - try - { - var response = this._response; - - var outputStream = response.OutputStream; - - // This is needed with compression - outputStream.Flush(); - outputStream.Dispose(); - - response.Close(); - } - catch (SocketException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in HttpListenerResponseWrapper"); - } - } - } - - public bool IsClosed - { - get; - private set; - } - - public void SetContentLength(long contentLength) - { - // you can happily set the Content-Length header in Asp.Net - // but HttpListener will complain if you do - you have to set ContentLength64 on the response. - // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header - //_response.ContentLength64 = contentLength; - } - - public void SetCookie(Cookie cookie) - { - var cookieStr = AsHeaderValue(cookie); - _response.Headers.Add("Set-Cookie", cookieStr); - } - - public bool SendChunked - { - get => _response.SendChunked; - set => _response.SendChunked = value; - } - - public bool KeepAlive { get; set; } - - public void ClearCookies() - { - } - const int StreamCopyToBufferSize = 81920; - public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) - { - // TODO - // return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); - var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - //if (count <= 0) - //{ - // allowAsync = true; - //} - - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsync) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - - using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} From e88f079da64a978d560f739e9bf17cd0a93d122a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 20:40:05 +0100 Subject: [PATCH 0082/6208] Remove websocket options and configure kestrel listen ports --- .../ApplicationHost.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ad6b8e84a8..7be39d6744 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -111,13 +111,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using ServiceStack; -using HttpResponse = MediaBrowser.Model.Net.HttpResponse; using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; namespace Emby.Server.Implementations @@ -623,21 +621,24 @@ namespace Emby.Server.Implementations FindParts(); Host = new WebHostBuilder() - .UseKestrel() + .UseKestrel(options => + { + options.Listen(IPAddress.Any, HttpPort); + options.Listen(IPAddress.Loopback, HttpPort); + // TODO certs + options.Listen(IPAddress.Any, HttpsPort, listenOptions => { listenOptions.UseHttps(); }); + options.Listen(IPAddress.Loopback, HttpsPort, listenOptions => { listenOptions.UseHttps(); }); + }) .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "jellyfin-web", "src")) - .UseUrls("http://localhost:8096") .ConfigureServices(services => { services.AddResponseCompression(); services.AddHttpContextAccessor(); services.TryAddSingleton(); }) - .Configure( app => + .Configure(app => { - app.UseWebSockets(new WebSocketOptions { - KeepAliveInterval = TimeSpan.FromMilliseconds(1000000000), - ReceiveBufferSize = 0x10000 - }); + app.UseWebSockets(); app.UseResponseCompression(); app.Use(ExecuteWebsocketHandlerAsync); From 4e229ad86b3a3b6024ca0c29c17e4aac5386a210 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 20:50:10 +0100 Subject: [PATCH 0083/6208] Fix PathInfo --- .../SocketSharp/WebSocketSharpRequest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index b07c4bfebb..146862e02b 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -341,10 +341,10 @@ namespace Emby.Server.Implementations.SocketSharp { var mode = HandlerFactoryPath; - var pos = request.Path.ToString().IndexOf("?", StringComparison.Ordinal); + var pos = RawUrl.IndexOf("?", StringComparison.Ordinal); if (pos != -1) { - var path = request.Path.ToString().Substring(0, pos); + var path = RawUrl.Substring(0, pos); this.pathInfo = GetPathInfo( path, mode, @@ -352,10 +352,10 @@ namespace Emby.Server.Implementations.SocketSharp } else { - this.pathInfo = request.Path.ToString(); + this.pathInfo = RawUrl; } - this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); + this.pathInfo = WebUtility.UrlDecode(pathInfo); this.pathInfo = NormalizePathInfo(pathInfo, mode); } From f1c93ae618f293eae4cc384fadfd4440c4e0cf2d Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 21:08:30 +0100 Subject: [PATCH 0084/6208] Remove SetContentLength and company --- .../HttpClientManager/HttpClientManager.cs | 1 - .../HttpServer/FileWriter.cs | 3 --- .../HttpServer/HttpListenerHost.cs | 6 +----- .../HttpServer/HttpResultFactory.cs | 13 ++++--------- .../HttpServer/RangeRequestWriter.cs | 3 +-- .../HttpServer/ResponseFilter.cs | 1 - .../HttpServer/StreamWriter.cs | 10 +--------- Emby.Server.Implementations/Services/HttpResult.cs | 7 +------ .../Services/ResponseHelper.cs | 7 ------- .../SocketSharp/WebSocketSharpResponse.cs | 8 -------- MediaBrowser.Model/Services/IRequest.cs | 2 -- 11 files changed, 8 insertions(+), 53 deletions(-) diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 834a494f81..ef82d1d9ee 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -327,7 +327,6 @@ namespace Emby.Server.Implementations.HttpClientManager } httpWebRequest.ContentType = contentType; - // httpWebRequest.ContentLength = bytes.Length; (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); } catch (Exception ex) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 835c6f52ef..1375089e39 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -63,8 +63,6 @@ namespace Emby.Server.Implementations.HttpServer if (string.IsNullOrWhiteSpace(rangeHeader)) { - // TODO - //Headers["Content-Length"] = TotalContentLength.ToString(UsCulture); StatusCode = HttpStatusCode.OK; } else @@ -99,7 +97,6 @@ namespace Emby.Server.Implementations.HttpServer // Content-Length is the length of what we're serving, not the original content var lengthString = RangeLength.ToString(UsCulture); - // TODO Headers["Content-Length"] = lengthString; var rangeString = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); Headers["Content-Range"] = rangeString; diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 12c23a8693..6b69fa0f4e 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -649,11 +649,6 @@ namespace Emby.Server.Implementations.HttpServer private static Task Write(IResponse response, string text) { var bOutput = Encoding.UTF8.GetBytes(text); - response.SetContentLength(bOutput.Length); - // TODO - response.Headers.Remove("Content-Length"); // DO NOT SET THIS, IT'S DONE AUTOMATICALLY BECAUSE MS ARE NOT STUPID - response.SendChunked = true; - return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); } @@ -672,6 +667,7 @@ namespace Emby.Server.Implementations.HttpServer } else { + // TODO what is this? var httpsUrl = url .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 5e9d2b4c36..09cdbc3c26 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.HttpServer content = Array.Empty(); } - result = new StreamWriter(content, contentType, contentLength); + result = new StreamWriter(content, contentType); } else { @@ -175,7 +175,7 @@ namespace Emby.Server.Implementations.HttpServer bytes = Array.Empty(); } - result = new StreamWriter(bytes, contentType, contentLength); + result = new StreamWriter(bytes, contentType); } else { @@ -334,13 +334,13 @@ namespace Emby.Server.Implementations.HttpServer if (isHeadRequest) { - var result = new StreamWriter(Array.Empty(), contentType, contentLength); + var result = new StreamWriter(Array.Empty(), contentType); AddResponseHeaders(result, responseHeaders); return result; } else { - var result = new StreamWriter(content, contentType, contentLength); + var result = new StreamWriter(content, contentType); AddResponseHeaders(result, responseHeaders); return result; } @@ -590,11 +590,6 @@ namespace Emby.Server.Implementations.HttpServer } else { - if (totalContentLength.HasValue) - { - // TODO responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture); - } - if (isHeadRequest) { using (stream) diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 9c8ab8d91c..8904e11d30 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -96,8 +96,7 @@ namespace Emby.Server.Implementations.HttpServer RangeLength = 1 + RangeEnd - RangeStart; // Content-Length is the length of what we're serving, not the original content - // TODO Headers["Content-Length"] = RangeLength.ToString(UsCulture); - Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); + Headers["Content-Range"] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; if (RangeStart > 0 && SourceStream.CanSeek) { diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index dbce3250d4..ae6a6576e0 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -57,7 +57,6 @@ namespace Emby.Server.Implementations.HttpServer if (length > 0) { - res.SetContentLength(length); res.SendChunked = false; } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 750b0f7958..90354385dc 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -53,11 +53,6 @@ namespace Emby.Server.Implementations.HttpServer SourceStream = source; Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - // TODO Headers["Content-Length"] = source.Length.ToString(UsCulture); - } } /// @@ -65,8 +60,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// The source. /// Type of the content. - /// The logger. - public StreamWriter(byte[] source, string contentType, int contentLength) + public StreamWriter(byte[] source, string contentType) { if (string.IsNullOrEmpty(contentType)) { @@ -76,8 +70,6 @@ namespace Emby.Server.Implementations.HttpServer SourceBytes = source; Headers["Content-Type"] = contentType; - - // TODO Headers["Content-Length"] = contentLength.ToString(UsCulture); } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index 296da2f7a0..b6758486ce 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -43,14 +43,9 @@ namespace Emby.Server.Implementations.Services { var contentLength = bytesResponse.Length; - if (response != null) - { - response.SetContentLength(contentLength); - } - if (contentLength > 0) { - await responseStream.WriteAsync(bytesResponse, 0, contentLength).ConfigureAwait(false); + await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); } return; } diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index dc99753477..0301ff335f 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -20,8 +20,6 @@ namespace Emby.Server.Implementations.Services { response.StatusCode = (int)HttpStatusCode.NoContent; } - - response.SetContentLength(0); return Task.CompletedTask; } @@ -55,7 +53,6 @@ namespace Emby.Server.Implementations.Services { if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { - response.SetContentLength(long.Parse(responseHeaders.Value)); continue; } @@ -104,7 +101,6 @@ namespace Emby.Server.Implementations.Services if (bytes != null) { response.ContentType = "application/octet-stream"; - response.SetContentLength(bytes.Length); if (bytes.Length > 0) { @@ -117,7 +113,6 @@ namespace Emby.Server.Implementations.Services if (responseText != null) { bytes = Encoding.UTF8.GetBytes(responseText); - response.SetContentLength(bytes.Length); if (bytes.Length > 0) { return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); @@ -149,8 +144,6 @@ namespace Emby.Server.Implementations.Services var contentLength = ms.Length; - response.SetContentLength(contentLength); - if (contentLength > 0) { await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs index c68b959846..f9ecb52a55 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -143,14 +143,6 @@ namespace Emby.Server.Implementations.SocketSharp private set; } - public void SetContentLength(long contentLength) - { - // you can happily set the Content-Length header in Asp.Net - // but HttpListener will complain if you do - you have to set ContentLength64 on the response. - // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header - //_response.ContentLength64 = contentLength; - } - public void SetCookie(Cookie cookie) { var cookieStr = AsHeaderValue(cookie); diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 909cebce3d..b4ad24fec8 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -145,8 +145,6 @@ namespace MediaBrowser.Model.Services /// bool IsClosed { get; } - void SetContentLength(long contentLength); - //Add Metadata to Response Dictionary Items { get; } From e342b7bc71f16286da699bcce299b76df63360dc Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 21:27:02 +0100 Subject: [PATCH 0085/6208] Extend the IHttpServer interface to avoid the typecasting --- .../ApplicationHost.cs | 4 ++-- .../HttpServer/IHttpListener.cs | 14 ------------ MediaBrowser.Controller/Net/IHttpServer.cs | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7be39d6744..a1ea179040 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -655,7 +655,7 @@ namespace Emby.Server.Implementations return; } - await ((HttpListenerHost)HttpServer).ProcessWebSocketRequest(context).ConfigureAwait(false); + await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); } public async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) { @@ -670,7 +670,7 @@ namespace Emby.Server.Implementations var localPath = context.Request.Path.ToString(); var req = new WebSocketSharpRequest(request, response, request.Path, Logger); - await ((HttpListenerHost)HttpServer).RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); + await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); } protected virtual IHttpClient CreateHttpClient() diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs index 8350913610..1ef65d9d73 100644 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.HttpServer @@ -28,18 +26,6 @@ namespace Emby.Server.Implementations.HttpServer /// The web socket handler. Action WebSocketConnected { get; set; } - /// - /// Gets or sets the web socket connecting. - /// - /// The web socket connecting. - Action WebSocketConnecting { get; set; } - - /// - /// Starts this instance. - /// - /// The URL prefixes. - void Start(IEnumerable urlPrefixes); - /// /// Stops this instance. /// diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index f413030076..cede9a98fb 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -35,5 +38,24 @@ namespace MediaBrowser.Controller.Net /// If set, all requests will respond with this message /// string GlobalResponse { get; set; } + + /// + /// Sends the http context to the socket listener + /// + /// + /// + Task ProcessWebSocketRequest(HttpContext ctx); + + /// + /// The HTTP request handler + /// + /// + /// + /// + /// + /// + /// + Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, + CancellationToken cancellationToken); } } From 148db8b81ac4bb52262e071e6920b1449c8611fe Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 21:51:02 +0100 Subject: [PATCH 0086/6208] Remove unused SharpSocket stuff --- .../SocketSharp/SharpWebSocket.cs | 44 +++---------------- .../SocketSharp/WebSocketSharpListener.cs | 1 - 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index eab903db8e..66c5cc334b 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.SocketSharp /// Gets or sets the web socket. /// /// The web socket. - private WebSocket WebSocket { get; set; } + private readonly WebSocket _webSocket; private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); @@ -30,45 +30,14 @@ namespace Emby.Server.Implementations.SocketSharp public SharpWebSocket(WebSocket socket, ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - WebSocket = socket ?? throw new ArgumentNullException(nameof(socket)); + _webSocket = socket ?? throw new ArgumentNullException(nameof(socket)); } - public Task StartReceive() - { - return _taskCompletionSource.Task; - } - - private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e) - { - _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty); - - // Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e) - { - _taskCompletionSource.TrySetResult(true); - - Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketMessage(SocketHttpListener.MessageEventArgs e) - { - if (OnReceiveBytes != null) - { - OnReceiveBytes(e.RawData); - } - } - - public Task ConnectAsServerAsync() - { - return Task.CompletedTask; - } /// /// Gets or sets the state. /// /// The state. - public WebSocketState State => WebSocket.State; + public WebSocketState State => _webSocket.State; /// /// Sends the async. @@ -79,7 +48,7 @@ namespace Emby.Server.Implementations.SocketSharp /// Task. public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); + return _webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); } /// @@ -91,7 +60,7 @@ namespace Emby.Server.Implementations.SocketSharp /// Task. public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); + return _webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); } /// @@ -117,9 +86,6 @@ namespace Emby.Server.Implementations.SocketSharp if (dispose) { _cancellationTokenSource.Cancel(); - - // TODO - // WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult(); } _disposed = true; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 6e5190efdb..7a6144fb23 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -71,7 +71,6 @@ using Microsoft.Extensions.Logging; var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); var socket = new SharpWebSocket(webSocketContext, _logger); - await socket.ConnectAsServerAsync().ConfigureAwait(false); WebSocketConnected(new WebSocketConnectEventArgs { From 9c02e99e35fa6811502944b19dd1479750ff8f20 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 22:40:25 +0100 Subject: [PATCH 0087/6208] Undo some of the span abuse --- .../ApplicationHost.cs | 2 +- .../Emby.Server.Implementations.csproj | 1 + .../SocketSharp/WebSocketSharpRequest.cs | 25 ++++++------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a1ea179040..0b4a2fd30e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1676,7 +1676,7 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = logPing, LogRequest = logPing, - TimeoutMs = 30000, + TimeoutMs = 5000, BufferContent = false, CancellationToken = cancellationToken diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index a4dac4a7f9..230e4892c7 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -40,6 +40,7 @@ + diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 146862e02b..35fff08216 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -273,11 +273,11 @@ namespace Emby.Server.Implementations.SocketSharp private static string GetQueryStringContentType(HttpRequest httpReq) { - string format = httpReq.Query["format"]; + ReadOnlySpan format = httpReq.Query["format"].ToString().AsSpan(); if (format == null) { const int formatMaxLength = 4; - string pi = httpReq.Path.ToString(); + ReadOnlySpan pi = httpReq.Path.ToString().AsSpan(); if (pi == null || pi.Length <= formatMaxLength) { return null; @@ -285,7 +285,7 @@ namespace Emby.Server.Implementations.SocketSharp if (pi[0] == '/') { - pi = pi.Substring(1); + pi = pi.Slice(1); } format = LeftPart(pi, '/'); @@ -296,11 +296,11 @@ namespace Emby.Server.Implementations.SocketSharp } format = LeftPart(format, '.'); - if (format.ToLower().Contains("json")) + if (format.Contains("json".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return "application/json"; } - else if (format.ToLower().Contains("xml")) + else if (format.Contains("xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return "application/xml"; } @@ -308,25 +308,14 @@ namespace Emby.Server.Implementations.SocketSharp return null; } - public static string LeftPart(string strVal, char needle) + public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle) { if (strVal == null) { return null; } - var pos = strVal.IndexOf(needle.ToString(), StringComparison.Ordinal); - return pos == -1 ? strVal : strVal.Substring(0, pos); - } - - public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle.ToString()); + var pos = strVal.IndexOf(needle); return pos == -1 ? strVal : strVal.Slice(0, pos); } From 5510e8ebee743a3d07571051660b514d6cce973c Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 22:53:59 +0100 Subject: [PATCH 0088/6208] Remove unused Cookies --- .../SocketSharp/WebSocketSharpRequest.cs | 19 ------------------- MediaBrowser.Model/Services/IRequest.cs | 2 -- 2 files changed, 21 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 35fff08216..38d0332308 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -412,25 +412,6 @@ namespace Emby.Server.Implementations.SocketSharp return path.Length > 1 ? path.TrimEnd('/') : "/"; } - private Dictionary cookies; - public IDictionary Cookies - { - get - { - if (cookies == null) - { - cookies = new Dictionary(); - foreach (var cookie in this.request.Cookies) - { - var httpCookie = cookie; - cookies[httpCookie.Key] = new Cookie(httpCookie.Key, httpCookie.Value, "", ""); - } - } - - return cookies; - } - } - public string UserAgent => request.Headers[HeaderNames.UserAgent]; public QueryParamCollection Headers => new QueryParamCollection(request.Headers); diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index b4ad24fec8..c7dccdb6fd 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -41,8 +41,6 @@ namespace MediaBrowser.Model.Services string UserAgent { get; } - IDictionary Cookies { get; } - /// /// The expected Response ContentType for this request /// From 848cfc32cc89327e16ff6ea281dc1d9b96cc4f7a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 26 Feb 2019 22:57:59 +0100 Subject: [PATCH 0089/6208] More cleanup --- .../HttpServer/HttpListenerHost.cs | 1 - .../SocketSharp/WebSocketSharpListener.cs | 13 ++----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 6b69fa0f4e..5b1cb433a9 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -803,7 +803,6 @@ namespace Emby.Server.Implementations.HttpServer private bool _disposed; private readonly object _disposeLock = new object(); - private readonly WebSocketSharpListener _websocketlistener; protected virtual void Dispose(bool disposing) { diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 7a6144fb23..77469244b9 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -17,8 +17,6 @@ using Microsoft.Extensions.Logging; { public class WebSocketSharpListener : IHttpListener { - private HttpListener _listener; - private readonly ILogger _logger; private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); @@ -86,14 +84,14 @@ using Microsoft.Extensions.Logging; do { - result = await webSocketContext.ReceiveAsync(buffer, CancellationToken.None); + result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); socket.OnReceiveBytes(buffer.Array); message.AddRange(buffer.Array.Take(result.Count)); } while (!result.EndOfMessage && result.MessageType != WebSocketMessageType.Close); socket.OnReceiveBytes(message.ToArray()); await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, CancellationToken.None); + result.CloseStatusDescription, _disposeCancellationToken); socket.Dispose(); } else @@ -109,16 +107,9 @@ using Microsoft.Extensions.Logging; } } - public void Start(IEnumerable urlPrefixes) - { - throw new NotImplementedException(); - } - public Task Stop() { _disposeCancellationTokenSource.Cancel(); - _listener?.Close(); - return Task.CompletedTask; } From 5ccba7cf8aab7895d16beb9e2d9d24408a361ac1 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Wed, 27 Feb 2019 00:20:04 +0100 Subject: [PATCH 0090/6208] Move .azure to .ci --- {.azure => .ci}/azure-pipelines.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.azure => .ci}/azure-pipelines.yml (100%) diff --git a/.azure/azure-pipelines.yml b/.ci/azure-pipelines.yml similarity index 100% rename from .azure/azure-pipelines.yml rename to .ci/azure-pipelines.yml From feb5b62ad4bf62e5d3585100b39643a41f27e626 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Wed, 27 Feb 2019 00:53:16 +0100 Subject: [PATCH 0091/6208] Enabled clean and updated the Github Connection Name --- .ci/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index a14cf0aa3f..e5845c0efa 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -29,7 +29,7 @@ jobs: maxParallel: 2 steps: - checkout: self - clean: false + clean: true submodules: true persistCredentials: false @@ -159,7 +159,7 @@ jobs: - task: DownloadGitHubReleases@0 displayName: Download ABI compatibility check tool from GitHub inputs: - connection: GitHub (EraYaN) + connection: Jellyfin GitHub userRepository: EraYaN/dotnet-compatibility defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag #version: # Required when defaultVersionType != Latest From 77addb22835478a32c1133cfd69ae0da2ec5edea Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 07:32:36 +0100 Subject: [PATCH 0092/6208] Remove SocketHttpListener --- .../Emby.Server.Implementations.csproj | 2 - .../SocketSharp/SharpWebSocket.cs | 3 +- .../SocketSharp/WebSocketSharpRequest.cs | 1 - MediaBrowser.sln | 379 ++++--- SocketHttpListener/ByteOrder.cs | 17 - SocketHttpListener/CloseEventArgs.cs | 79 -- SocketHttpListener/CloseStatusCode.cs | 94 -- SocketHttpListener/CompressionMethod.cs | 23 - SocketHttpListener/ErrorEventArgs.cs | 42 - SocketHttpListener/Ext.cs | 946 ------------------ SocketHttpListener/Fin.cs | 8 - SocketHttpListener/HttpBase.cs | 70 -- SocketHttpListener/HttpResponse.cs | 122 --- SocketHttpListener/Mask.cs | 8 - SocketHttpListener/MessageEventArgs.cs | 84 -- SocketHttpListener/Net/CookieHelper.cs | 141 --- SocketHttpListener/Opcode.cs | 43 - SocketHttpListener/PayloadData.cs | 130 --- SocketHttpListener/Properties/AssemblyInfo.cs | 21 - SocketHttpListener/Rsv.cs | 8 - SocketHttpListener/SocketHttpListener.csproj | 18 - SocketHttpListener/SocketStream.cs | 74 -- SocketHttpListener/WebSocket.cs | 777 -------------- SocketHttpListener/WebSocketException.cs | 61 -- SocketHttpListener/WebSocketFrame.cs | 432 -------- 25 files changed, 188 insertions(+), 3395 deletions(-) delete mode 100644 SocketHttpListener/ByteOrder.cs delete mode 100644 SocketHttpListener/CloseEventArgs.cs delete mode 100644 SocketHttpListener/CloseStatusCode.cs delete mode 100644 SocketHttpListener/CompressionMethod.cs delete mode 100644 SocketHttpListener/ErrorEventArgs.cs delete mode 100644 SocketHttpListener/Ext.cs delete mode 100644 SocketHttpListener/Fin.cs delete mode 100644 SocketHttpListener/HttpBase.cs delete mode 100644 SocketHttpListener/HttpResponse.cs delete mode 100644 SocketHttpListener/Mask.cs delete mode 100644 SocketHttpListener/MessageEventArgs.cs delete mode 100644 SocketHttpListener/Net/CookieHelper.cs delete mode 100644 SocketHttpListener/Opcode.cs delete mode 100644 SocketHttpListener/PayloadData.cs delete mode 100644 SocketHttpListener/Properties/AssemblyInfo.cs delete mode 100644 SocketHttpListener/Rsv.cs delete mode 100644 SocketHttpListener/SocketHttpListener.csproj delete mode 100644 SocketHttpListener/SocketStream.cs delete mode 100644 SocketHttpListener/WebSocket.cs delete mode 100644 SocketHttpListener/WebSocketException.cs delete mode 100644 SocketHttpListener/WebSocketFrame.cs diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 230e4892c7..d4db5f2fcc 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,7 +9,6 @@ - @@ -40,7 +39,6 @@ - diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index 66c5cc334b..dcbfb80487 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.SocketSharp /// The web socket. private readonly WebSocket _webSocket; - private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed = false; + private bool _disposed; public SharpWebSocket(WebSocket socket, ILogger logger) { diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 38d0332308..53dce667ba 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using SocketHttpListener.Net; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 62ae58d732..5edc1a25a6 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,192 +1,187 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDInfo", "BDInfo\BDInfo.csproj", "{88AE38DF-19D7-406F-A6A9-09527719A21E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocketHttpListener", "SocketHttpListener\SocketHttpListener.csproj", "{1D74413B-E7CF-455B-B021-F52BDF881542}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsoMounter", "Emby.IsoMounting\IsoMounter\IsoMounter.csproj", "{9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - SharedVersion.cs = SharedVersion.cs - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.Build.0 = Release|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.Build.0 = Release|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.Build.0 = Release|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.Build.0 = Release|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.Build.0 = Release|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Release|Any CPU.Build.0 = Release|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.Build.0 = Release|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.Build.0 = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.Build.0 = Release|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU - {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} - EndGlobalSection - GlobalSection(AutomaticVersions) = postSolution - UpdateAssemblyVersion = True - UpdateAssemblyFileVersion = True - UpdateAssemblyInfoVersion = True - AssemblyVersionSettings = None.None.None.None - AssemblyFileVersionSettings = None.None.None.None - AssemblyInfoVersionSettings = None.None.None.None - UpdatePackageVersion = False - AssemblyInfoVersionType = SettingsVersion - InheritWinAppVersionFrom = None - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.StandardHeader = $1 - $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n - $0.DotNetNamingPolicy = $2 - $2.DirectoryNamespaceAssociation = PrefixedHierarchical - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDInfo", "BDInfo\BDInfo.csproj", "{88AE38DF-19D7-406F-A6A9-09527719A21E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsoMounter", "Emby.IsoMounting\IsoMounter\IsoMounter.csproj", "{9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + SharedVersion.cs = SharedVersion.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.Build.0 = Release|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.Build.0 = Release|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.Build.0 = Release|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.Build.0 = Release|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU + {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} + EndGlobalSection + GlobalSection(AutomaticVersions) = postSolution + UpdateAssemblyVersion = True + UpdateAssemblyFileVersion = True + UpdateAssemblyInfoVersion = True + AssemblyVersionSettings = None.None.None.None + AssemblyFileVersionSettings = None.None.None.None + AssemblyInfoVersionSettings = None.None.None.None + UpdatePackageVersion = False + AssemblyInfoVersionType = SettingsVersion + InheritWinAppVersionFrom = None + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.StandardHeader = $1 + $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n + $0.DotNetNamingPolicy = $2 + $2.DirectoryNamespaceAssociation = PrefixedHierarchical + EndGlobalSection +EndGlobal diff --git a/SocketHttpListener/ByteOrder.cs b/SocketHttpListener/ByteOrder.cs deleted file mode 100644 index c04150c74b..0000000000 --- a/SocketHttpListener/ByteOrder.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian. - /// - public enum ByteOrder : byte - { - /// - /// Indicates a Little-endian. - /// - Little, - /// - /// Indicates a Big-endian. - /// - Big - } -} diff --git a/SocketHttpListener/CloseEventArgs.cs b/SocketHttpListener/CloseEventArgs.cs deleted file mode 100644 index c6460fd230..0000000000 --- a/SocketHttpListener/CloseEventArgs.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Text; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the WebSocket connection has been closed. - /// If you would like to get the reason for the close, you should access the or - /// property. - /// - public class CloseEventArgs : EventArgs - { - #region Private Fields - - private bool _clean; - private ushort _code; - private string _reason; - - #endregion - - #region Internal Constructors - - internal CloseEventArgs(PayloadData payload) - { - var data = payload.ApplicationData; - var len = data.Length; - _code = len > 1 - ? data.SubArray(0, 2).ToUInt16(ByteOrder.Big) - : (ushort)CloseStatusCode.NoStatusCode; - - _reason = len > 2 - ? GetUtf8String(data.SubArray(2, len - 2)) - : string.Empty; - } - - private static string GetUtf8String(byte[] bytes) - { - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); - } - - #endregion - - #region Public Properties - - /// - /// Gets the status code for the close. - /// - /// - /// A that represents the status code for the close if any. - /// - public ushort Code => _code; - - /// - /// Gets the reason for the close. - /// - /// - /// A that represents the reason for the close if any. - /// - public string Reason => _reason; - - /// - /// Gets a value indicating whether the WebSocket connection has been closed cleanly. - /// - /// - /// true if the WebSocket connection has been closed cleanly; otherwise, false. - /// - public bool WasClean - { - get => _clean; - - internal set => _clean = value; - } - - #endregion - } -} diff --git a/SocketHttpListener/CloseStatusCode.cs b/SocketHttpListener/CloseStatusCode.cs deleted file mode 100644 index 428595bb09..0000000000 --- a/SocketHttpListener/CloseStatusCode.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the status code for the WebSocket connection close. - /// - /// - /// - /// The values of the status code are defined in - /// Section 7.4 - /// of RFC 6455. - /// - /// - /// "Reserved value" must not be set as a status code in a close control frame - /// by an endpoint. It's designated for use in applications expecting a status - /// code to indicate that the connection was closed due to the system grounds. - /// - /// - public enum CloseStatusCode : ushort - { - /// - /// Equivalent to close status 1000. - /// Indicates a normal close. - /// - Normal = 1000, - /// - /// Equivalent to close status 1001. - /// Indicates that an endpoint is going away. - /// - Away = 1001, - /// - /// Equivalent to close status 1002. - /// Indicates that an endpoint is terminating the connection due to a protocol error. - /// - ProtocolError = 1002, - /// - /// Equivalent to close status 1003. - /// Indicates that an endpoint is terminating the connection because it has received - /// an unacceptable type message. - /// - IncorrectData = 1003, - /// - /// Equivalent to close status 1004. - /// Still undefined. Reserved value. - /// - Undefined = 1004, - /// - /// Equivalent to close status 1005. - /// Indicates that no status code was actually present. Reserved value. - /// - NoStatusCode = 1005, - /// - /// Equivalent to close status 1006. - /// Indicates that the connection was closed abnormally. Reserved value. - /// - Abnormal = 1006, - /// - /// Equivalent to close status 1007. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that contains a data that isn't consistent with the type of the message. - /// - InconsistentData = 1007, - /// - /// Equivalent to close status 1008. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that violates its policy. - /// - PolicyViolation = 1008, - /// - /// Equivalent to close status 1009. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that is too big to process. - /// - TooBig = 1009, - /// - /// Equivalent to close status 1010. - /// Indicates that the client is terminating the connection because it has expected - /// the server to negotiate one or more extension, but the server didn't return them - /// in the handshake response. - /// - IgnoreExtension = 1010, - /// - /// Equivalent to close status 1011. - /// Indicates that the server is terminating the connection because it has encountered - /// an unexpected condition that prevented it from fulfilling the request. - /// - ServerError = 1011, - /// - /// Equivalent to close status 1015. - /// Indicates that the connection was closed due to a failure to perform a TLS handshake. - /// Reserved value. - /// - TlsHandshakeFailure = 1015 - } -} diff --git a/SocketHttpListener/CompressionMethod.cs b/SocketHttpListener/CompressionMethod.cs deleted file mode 100644 index d6bcd63d89..0000000000 --- a/SocketHttpListener/CompressionMethod.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the compression method used to compress the message on the WebSocket - /// connection. - /// - /// - /// The values of the compression method are defined in - /// Compression - /// Extensions for WebSocket. - /// - public enum CompressionMethod : byte - { - /// - /// Indicates non compression. - /// - None, - /// - /// Indicates using DEFLATE. - /// - Deflate - } -} diff --git a/SocketHttpListener/ErrorEventArgs.cs b/SocketHttpListener/ErrorEventArgs.cs deleted file mode 100644 index 9502d2a156..0000000000 --- a/SocketHttpListener/ErrorEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the gets an error. - /// If you would like to get the error message, you should access the - /// property. - /// - public class ErrorEventArgs : EventArgs - { - #region Private Fields - - private string _message; - - #endregion - - #region Internal Constructors - - internal ErrorEventArgs(string message) - { - _message = message; - } - - #endregion - - #region Public Properties - - /// - /// Gets the error message. - /// - /// - /// A that represents the error message. - /// - public string Message => _message; - - #endregion - } -} diff --git a/SocketHttpListener/Ext.cs b/SocketHttpListener/Ext.cs deleted file mode 100644 index 697c5d5b70..0000000000 --- a/SocketHttpListener/Ext.cs +++ /dev/null @@ -1,946 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener -{ - /// - /// Provides a set of static methods for the websocket-sharp. - /// - public static class Ext - { - #region Private Const Fields - - private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; - - #endregion - - #region Private Methods - - private static MemoryStream compress(this Stream stream) - { - var output = new MemoryStream(); - if (stream.Length == 0) - return output; - - stream.Position = 0; - using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) - { - stream.CopyTo(ds); - //ds.Close(); // "BFINAL" set to 1. - output.Position = 0; - - return output; - } - } - - private static byte[] decompress(this byte[] value) - { - if (value.Length == 0) - return value; - - using (var input = new MemoryStream(value)) - { - return input.decompressToArray(); - } - } - - private static MemoryStream decompress(this Stream stream) - { - var output = new MemoryStream(); - if (stream.Length == 0) - return output; - - stream.Position = 0; - using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) - { - ds.CopyTo(output, true); - return output; - } - } - - private static byte[] decompressToArray(this Stream stream) - { - using (var decomp = stream.decompress()) - { - return decomp.ToArray(); - } - } - - private static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length) - { - var len = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false); - if (len < 1) - return buffer.SubArray(0, offset); - - var tmp = 0; - while (len < length) - { - tmp = await stream.ReadAsync(buffer, offset + len, length - len).ConfigureAwait(false); - if (tmp < 1) - { - break; - } - - len += tmp; - } - - return len < length - ? buffer.SubArray(0, offset + len) - : buffer; - } - - private static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length, Stream dest) - { - var bytes = await stream.ReadBytesAsync(buffer, offset, length).ConfigureAwait(false); - var len = bytes.Length; - dest.Write(bytes, 0, len); - - return len == offset + length; - } - - #endregion - - #region Internal Methods - - internal static async Task AppendAsync(this ushort code, string reason) - { - using (var buffer = new MemoryStream()) - { - var tmp = code.ToByteArrayInternally(ByteOrder.Big); - await buffer.WriteAsync(tmp, 0, 2).ConfigureAwait(false); - if (reason != null && reason.Length > 0) - { - tmp = Encoding.UTF8.GetBytes(reason); - await buffer.WriteAsync(tmp, 0, tmp.Length).ConfigureAwait(false); - } - - return buffer.ToArray(); - } - } - - internal static string CheckIfClosable(this WebSocketState state) - { - return state == WebSocketState.CloseSent - ? "While closing the WebSocket connection." - : state == WebSocketState.Closed - ? "The WebSocket connection has already been closed." - : null; - } - - internal static string CheckIfOpen(this WebSocketState state) - { - return state == WebSocketState.Connecting - ? "A WebSocket connection isn't established." - : state == WebSocketState.CloseSent - ? "While closing the WebSocket connection." - : state == WebSocketState.Closed - ? "The WebSocket connection has already been closed." - : null; - } - - internal static string CheckIfValidControlData(this byte[] data, string paramName) - { - return data.Length > 125 - ? string.Format("'{0}' length must be less.", paramName) - : null; - } - - internal static Stream Compress(this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.compress() - : stream; - } - - internal static bool Contains(this IEnumerable source, Func condition) - { - foreach (T elm in source) - if (condition(elm)) - return true; - - return false; - } - - internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition) - { - var readLen = 0; - var bufferLen = 256; - var buffer = new byte[bufferLen]; - while ((readLen = src.Read(buffer, 0, bufferLen)) > 0) - { - dest.Write(buffer, 0, readLen); - } - - if (setDefaultPosition) - dest.Position = 0; - } - - internal static byte[] Decompress(this byte[] value, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? value.decompress() - : value; - } - - internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.decompressToArray() - : stream.ToByteArray(); - } - - /// - /// Determines whether the specified equals the specified , - /// and invokes the specified Action<int> delegate at the same time. - /// - /// - /// true if equals ; - /// otherwise, false. - /// - /// - /// An to compare. - /// - /// - /// A to compare. - /// - /// - /// An Action<int> delegate that references the method(s) called at - /// the same time as comparing. An parameter to pass to - /// the method(s) is . - /// - /// - /// isn't between 0 and 255. - /// - internal static bool EqualsWith(this int value, char c, Action action) - { - if (value < 0 || value > 255) - throw new ArgumentOutOfRangeException(nameof(value)); - - action(value); - return value == c - 0; - } - - internal static string GetMessage(this CloseStatusCode code) - { - return code == CloseStatusCode.ProtocolError - ? "A WebSocket protocol error has occurred." - : code == CloseStatusCode.IncorrectData - ? "An incorrect data has been received." - : code == CloseStatusCode.Abnormal - ? "An exception has occurred." - : code == CloseStatusCode.InconsistentData - ? "An inconsistent data has been received." - : code == CloseStatusCode.PolicyViolation - ? "A policy violation has occurred." - : code == CloseStatusCode.TooBig - ? "A too big data has been received." - : code == CloseStatusCode.IgnoreExtension - ? "WebSocket client did not receive expected extension(s)." - : code == CloseStatusCode.ServerError - ? "WebSocket server got an internal error." - : code == CloseStatusCode.TlsHandshakeFailure - ? "An error has occurred while handshaking." - : string.Empty; - } - - internal static string GetNameInternal(this string nameAndValue, string separator) - { - var i = nameAndValue.IndexOf(separator); - return i > 0 - ? nameAndValue.Substring(0, i).Trim() - : null; - } - - internal static string GetValueInternal(this string nameAndValue, string separator) - { - var i = nameAndValue.IndexOf(separator); - return i >= 0 && i < nameAndValue.Length - 1 - ? nameAndValue.Substring(i + 1).Trim() - : null; - } - - internal static bool IsCompressionExtension(this string value, CompressionMethod method) - { - return value.StartsWith(method.ToExtensionString()); - } - - internal static bool IsPortNumber(this int value) - { - return value > 0 && value < 65536; - } - - internal static bool IsReserved(this ushort code) - { - return code == (ushort)CloseStatusCode.Undefined || - code == (ushort)CloseStatusCode.NoStatusCode || - code == (ushort)CloseStatusCode.Abnormal || - code == (ushort)CloseStatusCode.TlsHandshakeFailure; - } - - internal static bool IsReserved(this CloseStatusCode code) - { - return code == CloseStatusCode.Undefined || - code == CloseStatusCode.NoStatusCode || - code == CloseStatusCode.Abnormal || - code == CloseStatusCode.TlsHandshakeFailure; - } - - internal static bool IsText(this string value) - { - var len = value.Length; - for (var i = 0; i < len; i++) - { - char c = value[i]; - if (c < 0x20 && !"\r\n\t".Contains(c)) - return false; - - if (c == 0x7f) - return false; - - if (c == '\n' && ++i < len) - { - c = value[i]; - if (!" \t".Contains(c)) - return false; - } - } - - return true; - } - - internal static bool IsToken(this string value) - { - foreach (char c in value) - if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c)) - return false; - - return true; - } - - internal static string Quote(this string value) - { - return value.IsToken() - ? value - : string.Format("\"{0}\"", value.Replace("\"", "\\\"")); - } - - internal static Task ReadBytesAsync(this Stream stream, int length) - => stream.ReadBytesAsync(new byte[length], 0, length); - - internal static async Task ReadBytesAsync(this Stream stream, long length, int bufferLength) - { - using (var result = new MemoryStream()) - { - var count = length / bufferLength; - var rem = (int)(length % bufferLength); - - var buffer = new byte[bufferLength]; - var end = false; - for (long i = 0; i < count; i++) - { - if (!await stream.ReadBytesAsync(buffer, 0, bufferLength, result).ConfigureAwait(false)) - { - end = true; - break; - } - } - - if (!end && rem > 0) - { - await stream.ReadBytesAsync(new byte[rem], 0, rem, result).ConfigureAwait(false); - } - - return result.ToArray(); - } - } - - internal static string RemovePrefix(this string value, params string[] prefixes) - { - var i = 0; - foreach (var prefix in prefixes) - { - if (value.StartsWith(prefix)) - { - i = prefix.Length; - break; - } - } - - return i > 0 - ? value.Substring(i) - : value; - } - - internal static T[] Reverse(this T[] array) - { - var len = array.Length; - T[] reverse = new T[len]; - - var end = len - 1; - for (var i = 0; i <= end; i++) - reverse[i] = array[end - i]; - - return reverse; - } - - internal static IEnumerable SplitHeaderValue( - this string value, params char[] separator) - { - var len = value.Length; - var separators = new string(separator); - - var buffer = new StringBuilder(32); - var quoted = false; - var escaped = false; - - char c; - for (var i = 0; i < len; i++) - { - c = value[i]; - if (c == '"') - { - if (escaped) - escaped = !escaped; - else - quoted = !quoted; - } - else if (c == '\\') - { - if (i < len - 1 && value[i + 1] == '"') - escaped = true; - } - else if (separators.Contains(c)) - { - if (!quoted) - { - yield return buffer.ToString(); - buffer.Length = 0; - - continue; - } - } - else - { - } - - buffer.Append(c); - } - - if (buffer.Length > 0) - yield return buffer.ToString(); - } - - internal static byte[] ToByteArray(this Stream stream) - { - using (var output = new MemoryStream()) - { - stream.Position = 0; - stream.CopyTo(output); - - return output.ToArray(); - } - } - - internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order) - { - var bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - Array.Reverse(bytes); - - return bytes; - } - - internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order) - { - var bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - Array.Reverse(bytes); - - return bytes; - } - - internal static string ToExtensionString( - this CompressionMethod method, params string[] parameters) - { - if (method == CompressionMethod.None) - return string.Empty; - - var m = string.Format("permessage-{0}", method.ToString().ToLowerInvariant()); - if (parameters == null || parameters.Length == 0) - return m; - - return string.Format("{0}; {1}", m, parameters.ToString("; ")); - } - - internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) - { - src.ToHostOrder(srcOrder); - return BitConverter.ToUInt16(src, 0); - } - - internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) - { - src.ToHostOrder(srcOrder); - return BitConverter.ToUInt64(src, 0); - } - - internal static string TrimEndSlash(this string value) - { - value = value.TrimEnd('/'); - return value.Length > 0 - ? value - : "/"; - } - - internal static string Unquote(this string value) - { - var start = value.IndexOf('\"'); - var end = value.LastIndexOf('\"'); - if (start < end) - value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\""); - - return value.Trim(); - } - - internal static void WriteBytes(this Stream stream, byte[] value) - { - using (var src = new MemoryStream(value)) - { - src.CopyTo(stream); - } - } - - #endregion - - #region Public Methods - - /// - /// Determines whether the specified contains any of characters - /// in the specified array of . - /// - /// - /// true if contains any of ; - /// otherwise, false. - /// - /// - /// A to test. - /// - /// - /// An array of that contains characters to find. - /// - public static bool Contains(this string value, params char[] chars) - { - return chars == null || chars.Length == 0 - ? true - : value == null || value.Length == 0 - ? false - : value.IndexOfAny(chars) != -1; - } - - /// - /// Determines whether the specified contains the entry - /// with the specified . - /// - /// - /// true if contains the entry - /// with ; otherwise, false. - /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. - /// - public static bool Contains(this QueryParamCollection collection, string name) - { - return collection == null || collection.Count == 0 - ? false - : collection[name] != null; - } - - /// - /// Determines whether the specified contains the entry - /// with the specified both and . - /// - /// - /// true if contains the entry - /// with both and ; - /// otherwise, false. - /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. - /// - /// - /// A that represents the value of the entry to find. - /// - public static bool Contains(this QueryParamCollection collection, string name, string value) - { - if (collection == null || collection.Count == 0) - return false; - - var values = collection[name]; - if (values == null) - return false; - - foreach (var v in values.Split(',')) - if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; - } - - /// - /// Emits the specified EventHandler<TEventArgs> delegate - /// if it isn't . - /// - /// - /// An EventHandler<TEventArgs> to emit. - /// - /// - /// An from which emits this . - /// - /// - /// A TEventArgs that represents the event data. - /// - /// - /// The type of the event data generated by the event. - /// - public static void Emit( - this EventHandler eventHandler, object sender, TEventArgs e) - where TEventArgs : EventArgs - { - if (eventHandler != null) - eventHandler(sender, e); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// One of enum values, indicates the HTTP status codes. - /// - public static string GetDescription(this HttpStatusCode code) - { - return ((int)code).GetStatusDescription(); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// An that represents the HTTP status code. - /// - public static string GetStatusDescription(this int code) - { - switch (code) - { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-Authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Request Entity Too Large"; - case 414: return "Request-Uri Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "Http Version Not Supported"; - case 507: return "Insufficient Storage"; - } - - return string.Empty; - } - - /// - /// Determines whether the specified is host - /// (this computer architecture) byte order. - /// - /// - /// true if is host byte order; - /// otherwise, false. - /// - /// - /// One of the enum values, to test. - /// - public static bool IsHostOrder(this ByteOrder order) - { - // true : !(true ^ true) or !(false ^ false) - // false: !(true ^ false) or !(false ^ true) - return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); - } - - /// - /// Determines whether the specified is a predefined scheme. - /// - /// - /// true if is a predefined scheme; otherwise, false. - /// - /// - /// A to test. - /// - public static bool IsPredefinedScheme(this string value) - { - if (value == null || value.Length < 2) - return false; - - var c = value[0]; - if (c == 'h') - return value == "http" || value == "https"; - - if (c == 'w') - return value == "ws" || value == "wss"; - - if (c == 'f') - return value == "file" || value == "ftp"; - - if (c == 'n') - { - c = value[1]; - return c == 'e' - ? value == "news" || value == "net.pipe" || value == "net.tcp" - : value == "nntp"; - } - - return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); - } - - /// - /// Determines whether the specified is a URI string. - /// - /// - /// true if may be a URI string; otherwise, false. - /// - /// - /// A to test. - /// - public static bool MaybeUri(this string value) - { - if (value == null || value.Length == 0) - return false; - - var i = value.IndexOf(':'); - if (i == -1) - return false; - - if (i >= 10) - return false; - - return value.Substring(0, i).IsPredefinedScheme(); - } - - /// - /// Retrieves a sub-array from the specified . - /// A sub-array starts at the specified element position. - /// - /// - /// An array of T that receives a sub-array, or an empty array of T if any problems - /// with the parameters. - /// - /// - /// An array of T that contains the data to retrieve a sub-array. - /// - /// - /// An that contains the zero-based starting position of a sub-array - /// in . - /// - /// - /// An that contains the number of elements to retrieve a sub-array. - /// - /// - /// The type of elements in the . - /// - public static T[] SubArray(this T[] array, int startIndex, int length) - { - if (array == null || array.Length == 0) - return new T[0]; - - if (startIndex < 0 || length <= 0) - return new T[0]; - - if (startIndex + length > array.Length) - return new T[0]; - - if (startIndex == 0 && array.Length == length) - return array; - - T[] subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - /// - /// Converts the order of the specified array of to the host byte order. - /// - /// - /// An array of converted from . - /// - /// - /// An array of to convert. - /// - /// - /// One of the enum values, indicates the byte order of - /// . - /// - /// - /// is . - /// - public static void ToHostOrder(this byte[] src, ByteOrder srcOrder) - { - if (src == null) - { - throw new ArgumentNullException(nameof(src)); - } - - if (src.Length > 1 && !srcOrder.IsHostOrder()) - { - Array.Reverse(src); - } - } - - /// - /// Converts the specified to a that - /// concatenates the each element of across the specified - /// . - /// - /// - /// A converted from , - /// or if is empty. - /// - /// - /// An array of T to convert. - /// - /// - /// A that represents the separator string. - /// - /// - /// The type of elements in . - /// - /// - /// is . - /// - public static string ToString(this T[] array, string separator) - { - if (array == null) - throw new ArgumentNullException(nameof(array)); - - var len = array.Length; - if (len == 0) - return string.Empty; - - if (separator == null) - separator = string.Empty; - - var buff = new StringBuilder(64); - (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); - - buff.Append(array[len - 1].ToString()); - return buff.ToString(); - } - - /// - /// Executes the specified Action<int> delegate times. - /// - /// - /// An is the number of times to execute. - /// - /// - /// An Action<int> delegate that references the method(s) to execute. - /// An parameter to pass to the method(s) is the zero-based count of - /// iteration. - /// - public static void Times(this int n, Action action) - { - if (n > 0 && action != null) - for (int i = 0; i < n; i++) - action(i); - } - - /// - /// Converts the specified to a . - /// - /// - /// A converted from , or - /// if isn't successfully converted. - /// - /// - /// A to convert. - /// - public static Uri ToUri(this string uriString) - { - return Uri.TryCreate( - uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out var res) - ? res - : null; - } - - /// - /// URL-decodes the specified . - /// - /// - /// A that receives the decoded string, or the - /// if it's or empty. - /// - /// - /// A to decode. - /// - public static string UrlDecode(this string value) - { - return value == null || value.Length == 0 - ? value - : WebUtility.UrlDecode(value); - } - - #endregion - } -} diff --git a/SocketHttpListener/Fin.cs b/SocketHttpListener/Fin.cs deleted file mode 100644 index c8ffbf2b2f..0000000000 --- a/SocketHttpListener/Fin.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Fin : byte - { - More = 0x0, - Final = 0x1 - } -} diff --git a/SocketHttpListener/HttpBase.cs b/SocketHttpListener/HttpBase.cs deleted file mode 100644 index c386b93744..0000000000 --- a/SocketHttpListener/HttpBase.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener -{ - internal abstract class HttpBase - { - #region Private Fields - - private QueryParamCollection _headers; - private Version _version; - - #endregion - - #region Protected Fields - - protected const string CrLf = "\r\n"; - - #endregion - - #region Protected Constructors - - protected HttpBase(Version version, QueryParamCollection headers) - { - _version = version; - _headers = headers; - } - - #endregion - - #region Public Properties - - public QueryParamCollection Headers => _headers; - - public Version ProtocolVersion => _version; - - #endregion - - #region Private Methods - - private static Encoding getEncoding(string contentType) - { - if (contentType == null || contentType.Length == 0) - return Encoding.UTF8; - - var i = contentType.IndexOf("charset=", StringComparison.Ordinal); - if (i == -1) - return Encoding.UTF8; - - var charset = contentType.Substring(i + 8); - i = charset.IndexOf(';'); - if (i != -1) - charset = charset.Substring(0, i).TrimEnd(); - - return Encoding.GetEncoding(charset.Trim('"')); - } - - #endregion - - #region Public Methods - - public byte[] ToByteArray() - { - return Encoding.UTF8.GetBytes(ToString()); - } - - #endregion - } -} diff --git a/SocketHttpListener/HttpResponse.cs b/SocketHttpListener/HttpResponse.cs deleted file mode 100644 index a33b942959..0000000000 --- a/SocketHttpListener/HttpResponse.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net; - -namespace SocketHttpListener -{ - // TODO what is the point of this class? - internal class HttpResponse : HttpBase - { - #region Private Fields - - private string _code; - private string _reason; - - #endregion - - #region Private Constructors - - private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) - : base(version, headers) - { - _code = code; - _reason = reason; - } - - #endregion - - #region Internal Constructors - - internal HttpResponse(HttpStatusCode code) - : this(code, code.GetDescription()) - { - } - - internal HttpResponse(HttpStatusCode code, string reason) - : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) - { - Headers["Server"] = "websocket-sharp/1.0"; - } - - #endregion - - #region Public Properties - - public CookieCollection Cookies => GetCookies(Headers, true); - - private static CookieCollection GetCookies(QueryParamCollection headers, bool response) - { - var name = response ? "Set-Cookie" : "Cookie"; - return headers == null || !headers.Contains(name) - ? new CookieCollection() - : CookieHelper.Parse(headers[name], response); - } - - public bool IsProxyAuthenticationRequired => _code == "407"; - - public bool IsUnauthorized => _code == "401"; - - public bool IsWebSocketResponse - { - get - { - var headers = Headers; - return ProtocolVersion > HttpVersion.Version10 && - _code == "101" && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); - } - } - - public string Reason => _reason; - - public string StatusCode => _code; - - #endregion - - #region Internal Methods - - internal static HttpResponse CreateCloseResponse(HttpStatusCode code) - { - var res = new HttpResponse(code); - res.Headers["Connection"] = "close"; - - return res; - } - - #endregion - - #region Public Methods - - public void SetCookies(CookieCollection cookies) - { - if (cookies == null || cookies.Count == 0) - return; - - var headers = Headers; - var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); - - foreach (var cookie in sorted) - headers.Add("Set-Cookie", cookie.ToString()); - } - - public override string ToString() - { - var output = new StringBuilder(64); - output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); - - var headers = Headers; - foreach (var key in headers.Keys) - output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); - - output.Append(CrLf); - - return output.ToString(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Mask.cs b/SocketHttpListener/Mask.cs deleted file mode 100644 index c56f5b08c0..0000000000 --- a/SocketHttpListener/Mask.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Mask : byte - { - Unmask = 0x0, - Mask = 0x1 - } -} diff --git a/SocketHttpListener/MessageEventArgs.cs b/SocketHttpListener/MessageEventArgs.cs deleted file mode 100644 index 8e2151cb7a..0000000000 --- a/SocketHttpListener/MessageEventArgs.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Text; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the receives - /// a text or binary data frame. - /// If you want to get the received data, you access the or - /// property. - /// - public class MessageEventArgs : EventArgs - { - #region Private Fields - - private string _data; - private Opcode _opcode; - private byte[] _rawData; - - #endregion - - #region Internal Constructors - - internal MessageEventArgs(Opcode opcode, byte[] data) - { - _opcode = opcode; - _rawData = data; - _data = convertToString(opcode, data); - } - - internal MessageEventArgs(Opcode opcode, PayloadData payload) - { - _opcode = opcode; - _rawData = payload.ApplicationData; - _data = convertToString(opcode, _rawData); - } - - #endregion - - #region Public Properties - - /// - /// Gets the received data as a . - /// - /// - /// A that contains the received data. - /// - public string Data => _data; - - /// - /// Gets the received data as an array of . - /// - /// - /// An array of that contains the received data. - /// - public byte[] RawData => _rawData; - - /// - /// Gets the type of the received data. - /// - /// - /// One of the values, indicates the type of the received data. - /// - public Opcode Type => _opcode; - - #endregion - - #region Private Methods - - private static string convertToString(Opcode opcode, byte[] data) - { - return data.Length == 0 - ? string.Empty - : opcode == Opcode.Text - ? Encoding.UTF8.GetString(data, 0, data.Length) - : opcode.ToString(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Net/CookieHelper.cs b/SocketHttpListener/Net/CookieHelper.cs deleted file mode 100644 index 3ad76ff238..0000000000 --- a/SocketHttpListener/Net/CookieHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - public static class CookieHelper - { - internal static CookieCollection Parse(string value, bool response) - { - return response - ? parseResponse(value) - : null; - } - - private static string[] splitCookieHeaderValue(string value) - { - return new List(value.SplitHeaderValue(',', ';')).ToArray(); - } - - private static CookieCollection parseResponse(string value) - { - var cookies = new CookieCollection(); - - Cookie cookie = null; - var pairs = splitCookieHeaderValue(value); - for (int i = 0; i < pairs.Length; i++) - { - var pair = pairs[i].Trim(); - if (pair.Length == 0) - continue; - - if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Version = int.Parse(pair.GetValueInternal("=").Trim('"')); - } - else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase)) - { - var buffer = new StringBuilder(pair.GetValueInternal("="), 32); - if (i < pairs.Length - 1) - buffer.AppendFormat(", {0}", pairs[++i].Trim()); - - if (!DateTime.TryParseExact( - buffer.ToString(), - new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, - new CultureInfo("en-US"), - DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, - out var expires)) - expires = DateTime.Now; - - if (cookie != null && cookie.Expires == DateTime.MinValue) - cookie.Expires = expires.ToLocalTime(); - } - else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase)) - { - var max = int.Parse(pair.GetValueInternal("=").Trim('"')); - var expires = DateTime.Now.AddSeconds((double)max); - if (cookie != null) - cookie.Expires = expires; - } - else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Path = pair.GetValueInternal("="); - } - else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Domain = pair.GetValueInternal("="); - } - else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase)) - { - var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) - ? "\"\"" - : pair.GetValueInternal("="); - - if (cookie != null) - cookie.Port = port; - } - else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Comment = pair.GetValueInternal("=").UrlDecode(); - } - else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri(); - } - else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Discard = true; - } - else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Secure = true; - } - else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.HttpOnly = true; - } - else - { - if (cookie != null) - cookies.Add(cookie); - - string name; - string val = string.Empty; - - var pos = pair.IndexOf('='); - if (pos == -1) - { - name = pair; - } - else if (pos == pair.Length - 1) - { - name = pair.Substring(0, pos).TrimEnd(' '); - } - else - { - name = pair.Substring(0, pos).TrimEnd(' '); - val = pair.Substring(pos + 1).TrimStart(' '); - } - - cookie = new Cookie(name, val); - } - } - - if (cookie != null) - cookies.Add(cookie); - - return cookies; - } - } -} diff --git a/SocketHttpListener/Opcode.cs b/SocketHttpListener/Opcode.cs deleted file mode 100644 index 69cf3f3728..0000000000 --- a/SocketHttpListener/Opcode.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the opcode that indicates the type of a WebSocket frame. - /// - /// - /// The values of the opcode are defined in - /// Section 5.2 of RFC 6455. - /// - public enum Opcode : byte - { - /// - /// Equivalent to numeric value 0. - /// Indicates a continuation frame. - /// - Cont = 0x0, - /// - /// Equivalent to numeric value 1. - /// Indicates a text frame. - /// - Text = 0x1, - /// - /// Equivalent to numeric value 2. - /// Indicates a binary frame. - /// - Binary = 0x2, - /// - /// Equivalent to numeric value 8. - /// Indicates a connection close frame. - /// - Close = 0x8, - /// - /// Equivalent to numeric value 9. - /// Indicates a ping frame. - /// - Ping = 0x9, - /// - /// Equivalent to numeric value 10. - /// Indicates a pong frame. - /// - Pong = 0xa - } -} diff --git a/SocketHttpListener/PayloadData.cs b/SocketHttpListener/PayloadData.cs deleted file mode 100644 index 6d15a6bcb1..0000000000 --- a/SocketHttpListener/PayloadData.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace SocketHttpListener -{ - internal class PayloadData : IEnumerable - { - #region Private Fields - - private byte[] _applicationData; - private byte[] _extensionData; - private bool _masked; - - #endregion - - #region Public Const Fields - - public const ulong MaxLength = long.MaxValue; - - #endregion - - #region Public Constructors - - public PayloadData() - : this(new byte[0], new byte[0], false) - { - } - - public PayloadData(byte[] applicationData) - : this(new byte[0], applicationData, false) - { - } - - public PayloadData(string applicationData) - : this(new byte[0], Encoding.UTF8.GetBytes(applicationData), false) - { - } - - public PayloadData(byte[] applicationData, bool masked) - : this(new byte[0], applicationData, masked) - { - } - - public PayloadData(byte[] extensionData, byte[] applicationData, bool masked) - { - _extensionData = extensionData; - _applicationData = applicationData; - _masked = masked; - } - - #endregion - - #region Internal Properties - - internal bool ContainsReservedCloseStatusCode => - _applicationData.Length > 1 && - _applicationData.SubArray(0, 2).ToUInt16(ByteOrder.Big).IsReserved(); - - #endregion - - #region Public Properties - - public byte[] ApplicationData => _applicationData; - - public byte[] ExtensionData => _extensionData; - - public bool IsMasked => _masked; - - public ulong Length => (ulong)(_extensionData.Length + _applicationData.Length); - - #endregion - - #region Private Methods - - private static void mask(byte[] src, byte[] key) - { - for (long i = 0; i < src.Length; i++) - src[i] = (byte)(src[i] ^ key[i % 4]); - } - - #endregion - - #region Public Methods - - public IEnumerator GetEnumerator() - { - foreach (byte b in _extensionData) - yield return b; - - foreach (byte b in _applicationData) - yield return b; - } - - public void Mask(byte[] maskingKey) - { - if (_extensionData.Length > 0) - mask(_extensionData, maskingKey); - - if (_applicationData.Length > 0) - mask(_applicationData, maskingKey); - - _masked = !_masked; - } - - public byte[] ToByteArray() - { - return _extensionData.Length > 0 - ? new List(this).ToArray() - : _applicationData; - } - - public override string ToString() - { - return BitConverter.ToString(ToByteArray()); - } - - #endregion - - #region Explicitly Implemented Interface Members - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Properties/AssemblyInfo.cs b/SocketHttpListener/Properties/AssemblyInfo.cs deleted file mode 100644 index a69bd176f7..0000000000 --- a/SocketHttpListener/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +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("SocketHttpListener")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media 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)] diff --git a/SocketHttpListener/Rsv.cs b/SocketHttpListener/Rsv.cs deleted file mode 100644 index 87283791ec..0000000000 --- a/SocketHttpListener/Rsv.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Rsv : byte - { - Off = 0x0, - On = 0x1 - } -} diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj deleted file mode 100644 index e700540a91..0000000000 --- a/SocketHttpListener/SocketHttpListener.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - netstandard2.0 - true - false - - - diff --git a/SocketHttpListener/SocketStream.cs b/SocketHttpListener/SocketStream.cs deleted file mode 100644 index f51fde97ef..0000000000 --- a/SocketHttpListener/SocketStream.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.IO; -using System.Net.Sockets; - -namespace SocketHttpListener -{ - public class SocketStream : Stream - { - private readonly Socket _socket; - - public SocketStream(Socket socket, bool ownsSocket) - { - _socket = socket; - } - - public override void Flush() - { - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _socket.Send(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginSend(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - _socket.EndSend(asyncResult); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _socket.Receive(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginReceive(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return _socket.EndReceive(asyncResult); - } - } -} diff --git a/SocketHttpListener/WebSocket.cs b/SocketHttpListener/WebSocket.cs deleted file mode 100644 index afce871fd6..0000000000 --- a/SocketHttpListener/WebSocket.cs +++ /dev/null @@ -1,777 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener -{ - /// - /// Implements the WebSocket interface. - /// - /// - /// The WebSocket class provides a set of methods and properties for two-way communication using - /// the WebSocket protocol (RFC 6455). - /// - public class WebSocket : IDisposable - { - #region Private Fields - - private Action _closeContext; - private CompressionMethod _compression; - private WebSocketContext _context; - private CookieCollection _cookies; - private AutoResetEvent _exitReceiving; - private object _forConn; - private readonly SemaphoreSlim _forEvent = new SemaphoreSlim(1, 1); - private object _forMessageEventQueue; - private readonly SemaphoreSlim _forSend = new SemaphoreSlim(1, 1); - private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private Queue _messageEventQueue; - private string _protocol; - private volatile WebSocketState _readyState; - private AutoResetEvent _receivePong; - private bool _secure; - private Stream _stream; - private const string _version = "13"; - - #endregion - - #region Internal Fields - - internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. - - #endregion - - #region Internal Constructors - - // As server - internal WebSocket(string protocol) - { - _protocol = protocol; - } - - public void SetContext(HttpListenerWebSocketContext context, Action closeContextFn, Stream stream) - { - _context = context; - - _closeContext = closeContextFn; - _secure = context.IsSecureConnection; - _stream = stream; - - init(); - } - - // In the .NET Framework, this pulls the value from a P/Invoke. Here we just hardcode it to a reasonable default. - public static TimeSpan DefaultKeepAliveInterval => TimeSpan.FromSeconds(30); - - #endregion - - /// - /// Gets the state of the WebSocket connection. - /// - /// - /// One of the enum values, indicates the state of the WebSocket - /// connection. The default value is . - /// - public WebSocketState ReadyState => _readyState; - - #region Public Events - - /// - /// Occurs when the WebSocket connection has been closed. - /// - public event EventHandler OnClose; - - /// - /// Occurs when the gets an error. - /// - public event EventHandler OnError; - - /// - /// Occurs when the receives a message. - /// - public event EventHandler OnMessage; - - /// - /// Occurs when the WebSocket connection has been established. - /// - public event EventHandler OnOpen; - - #endregion - - #region Private Methods - - private async Task CloseAsync(CloseStatusCode code, string reason, bool wait) - { - await CloseAsync(new PayloadData( - await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)), - !code.IsReserved(), - wait).ConfigureAwait(false); - } - - private async Task CloseAsync(PayloadData payload, bool send, bool wait) - { - lock (_forConn) - { - if (_readyState == WebSocketState.CloseSent || _readyState == WebSocketState.Closed) - { - return; - } - - _readyState = WebSocketState.CloseSent; - } - - var e = new CloseEventArgs(payload) - { - WasClean = await CloseHandshakeAsync( - send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, - wait ? 1000 : 0).ConfigureAwait(false) - }; - - _readyState = WebSocketState.Closed; - try - { - OnClose.Emit(this, e); - } - catch (Exception ex) - { - error("An exception has occurred while OnClose.", ex); - } - } - - private async Task CloseHandshakeAsync(byte[] frameAsBytes, int millisecondsTimeout) - { - var sent = frameAsBytes != null && await WriteBytesAsync(frameAsBytes).ConfigureAwait(false); - var received = - millisecondsTimeout == 0 || - (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); - - closeServerResources(); - - if (_receivePong != null) - { - _receivePong.Dispose(); - _receivePong = null; - } - - if (_exitReceiving != null) - { - _exitReceiving.Dispose(); - _exitReceiving = null; - } - - var result = sent && received; - - return result; - } - - // As server - private void closeServerResources() - { - if (_closeContext == null) - return; - - try - { - _closeContext(); - } - catch (SocketException) - { - // it could be unable to send the handshake response - } - - _closeContext = null; - _stream = null; - _context = null; - } - - private async Task ConcatenateFragmentsIntoAsync(Stream dest) - { - while (true) - { - var frame = await WebSocketFrame.ReadAsync(_stream, true).ConfigureAwait(false); - if (frame.IsFinal) - { - /* FINAL */ - - // CONT - if (frame.IsContinuation) - { - dest.WriteBytes(frame.PayloadData.ApplicationData); - break; - } - - // PING - if (frame.IsPing) - { - processPingFrame(frame); - continue; - } - - // PONG - if (frame.IsPong) - { - processPongFrame(frame); - continue; - } - - // CLOSE - if (frame.IsClose) - return await ProcessCloseFrameAsync(frame).ConfigureAwait(false); - } - else - { - /* MORE */ - - // CONT - if (frame.IsContinuation) - { - dest.WriteBytes(frame.PayloadData.ApplicationData); - continue; - } - } - - // ? - return await ProcessUnsupportedFrameAsync( - frame, - CloseStatusCode.IncorrectData, - "An incorrect data has been received while receiving fragmented data.").ConfigureAwait(false); - } - - return true; - } - - // As server - private HttpResponse createHandshakeCloseResponse(HttpStatusCode code) - { - var res = HttpResponse.CreateCloseResponse(code); - res.Headers["Sec-WebSocket-Version"] = _version; - - return res; - } - - private MessageEventArgs dequeueFromMessageEventQueue() - { - lock (_forMessageEventQueue) - return _messageEventQueue.Count > 0 - ? _messageEventQueue.Dequeue() - : null; - } - - private void enqueueToMessageEventQueue(MessageEventArgs e) - { - lock (_forMessageEventQueue) - _messageEventQueue.Enqueue(e); - } - - private void error(string message, Exception exception) - { - try - { - if (exception != null) - { - message += ". Exception.Message: " + exception.Message; - } - OnError.Emit(this, new ErrorEventArgs(message)); - } - catch (Exception) - { - } - } - - private void error(string message) - { - try - { - OnError.Emit(this, new ErrorEventArgs(message)); - } - catch (Exception) - { - } - } - - private void init() - { - _compression = CompressionMethod.None; - _cookies = new CookieCollection(); - _forConn = new object(); - _messageEventQueue = new Queue(); - _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; - _readyState = WebSocketState.Connecting; - } - - private async Task OpenAsync() - { - try - { - startReceiving(); - - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while opening.").ConfigureAwait(false); - } - - await _forEvent.WaitAsync().ConfigureAwait(false); - try - { - OnOpen?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while OnOpen.").ConfigureAwait(false); - } - finally - { - _forEvent.Release(); - } - } - - private async Task ProcessCloseFrameAsync(WebSocketFrame frame) - { - var payload = frame.PayloadData; - await CloseAsync(payload, !payload.ContainsReservedCloseStatusCode, false).ConfigureAwait(false); - - return false; - } - - private bool processDataFrame(WebSocketFrame frame) - { - var e = frame.IsCompressed - ? new MessageEventArgs( - frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) - : new MessageEventArgs(frame.Opcode, frame.PayloadData); - - enqueueToMessageEventQueue(e); - return true; - } - - private async Task ProcessExceptionAsync(Exception exception, string message) - { - var code = CloseStatusCode.Abnormal; - var reason = message; - if (exception is WebSocketException) - { - var wsex = (WebSocketException)exception; - code = wsex.Code; - reason = wsex.Message; - } - - error(message ?? code.GetMessage(), exception); - if (_readyState == WebSocketState.Connecting) - { - await CloseAsync(HttpStatusCode.BadRequest).ConfigureAwait(false); - } - else - { - await CloseAsync(code, reason ?? code.GetMessage(), false).ConfigureAwait(false); - } - } - - private Task ProcessFragmentedFrameAsync(WebSocketFrame frame) - { - return frame.IsContinuation // Not first fragment - ? Task.FromResult(true) - : ProcessFragmentsAsync(frame); - } - - private async Task ProcessFragmentsAsync(WebSocketFrame first) - { - using (var buff = new MemoryStream()) - { - buff.WriteBytes(first.PayloadData.ApplicationData); - if (!await ConcatenateFragmentsIntoAsync(buff).ConfigureAwait(false)) - { - return false; - } - - byte[] data; - if (_compression != CompressionMethod.None) - { - data = buff.DecompressToArray(_compression); - } - else - { - data = buff.ToArray(); - } - - enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data)); - return true; - } - } - - private bool processPingFrame(WebSocketFrame frame) - { - return true; - } - - private bool processPongFrame(WebSocketFrame frame) - { - _receivePong.Set(); - - return true; - } - - private async Task ProcessUnsupportedFrameAsync(WebSocketFrame frame, CloseStatusCode code, string reason) - { - await ProcessExceptionAsync(new WebSocketException(code, reason), null).ConfigureAwait(false); - - return false; - } - - private Task ProcessWebSocketFrameAsync(WebSocketFrame frame) - { - // TODO: @bond change to if/else chain - return frame.IsCompressed && _compression == CompressionMethod.None - ? ProcessUnsupportedFrameAsync( - frame, - CloseStatusCode.IncorrectData, - "A compressed data has been received without available decompression method.") - : frame.IsFragmented - ? ProcessFragmentedFrameAsync(frame) - : frame.IsData - ? Task.FromResult(processDataFrame(frame)) - : frame.IsPing - ? Task.FromResult(processPingFrame(frame)) - : frame.IsPong - ? Task.FromResult(processPongFrame(frame)) - : frame.IsClose - ? ProcessCloseFrameAsync(frame) - : ProcessUnsupportedFrameAsync(frame, CloseStatusCode.PolicyViolation, null); - } - - private async Task SendAsync(Opcode opcode, Stream stream) - { - await _forSend.WaitAsync().ConfigureAwait(false); - try - { - var src = stream; - var compressed = false; - var sent = false; - try - { - if (_compression != CompressionMethod.None) - { - stream = stream.Compress(_compression); - compressed = true; - } - - sent = await SendAsync(opcode, Mask.Unmask, stream, compressed).ConfigureAwait(false); - if (!sent) - error("Sending a data has been interrupted."); - } - catch (Exception ex) - { - error("An exception has occurred while sending a data.", ex); - } - finally - { - if (compressed) - stream.Dispose(); - - src.Dispose(); - } - - return sent; - } - finally - { - _forSend.Release(); - } - } - - private async Task SendAsync(Opcode opcode, Mask mask, Stream stream, bool compressed) - { - var len = stream.Length; - - /* Not fragmented */ - - if (len == 0) - return await SendAsync(Fin.Final, opcode, mask, new byte[0], compressed).ConfigureAwait(false); - - var quo = len / FragmentLength; - var rem = (int)(len % FragmentLength); - - byte[] buff = null; - if (quo == 0) - { - buff = new byte[rem]; - return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && - await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); - } - - buff = new byte[FragmentLength]; - if (quo == 1 && rem == 0) - return await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) == FragmentLength && - await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); - - /* Send fragmented */ - - // Begin - if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || - !await SendAsync(Fin.More, opcode, mask, buff, compressed).ConfigureAwait(false)) - return false; - - var n = rem == 0 ? quo - 2 : quo - 1; - for (long i = 0; i < n; i++) - if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || - !await SendAsync(Fin.More, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false)) - return false; - - // End - if (rem == 0) - rem = FragmentLength; - else - buff = new byte[rem]; - - return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && - await SendAsync(Fin.Final, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false); - } - - private Task SendAsync(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) - { - lock (_forConn) - { - if (_readyState != WebSocketState.Open) - { - return Task.FromResult(false); - } - - return WriteBytesAsync( - WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); - } - } - - // As server - private Task SendHttpResponseAsync(HttpResponse response) - => WriteBytesAsync(response.ToByteArray()); - - private void startReceiving() - { - if (_messageEventQueue.Count > 0) - { - _messageEventQueue.Clear(); - } - - _exitReceiving = new AutoResetEvent(false); - _receivePong = new AutoResetEvent(false); - - Action receive = null; - receive = async () => await WebSocketFrame.ReadAsync( - _stream, - true, - async frame => - { - if (await ProcessWebSocketFrameAsync(frame).ConfigureAwait(false) && _readyState != WebSocketState.Closed) - { - receive(); - - if (!frame.IsData) - { - return; - } - - await _forEvent.WaitAsync().ConfigureAwait(false); - - try - { - var e = dequeueFromMessageEventQueue(); - if (e != null && _readyState == WebSocketState.Open) - { - OnMessage.Emit(this, e); - } - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while OnMessage.").ConfigureAwait(false); - } - finally - { - _forEvent.Release(); - } - - } - else if (_exitReceiving != null) - { - _exitReceiving.Set(); - } - }, - async ex => await ProcessExceptionAsync(ex, "An exception has occurred while receiving a message.")).ConfigureAwait(false); - - receive(); - } - - private async Task WriteBytesAsync(byte[] data) - { - try - { - await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); - return true; - } - catch (Exception) - { - return false; - } - } - - #endregion - - #region Internal Methods - - // As server - internal async Task CloseAsync(HttpResponse response) - { - _readyState = WebSocketState.CloseSent; - await SendHttpResponseAsync(response).ConfigureAwait(false); - - closeServerResources(); - - _readyState = WebSocketState.Closed; - } - - // As server - internal Task CloseAsync(HttpStatusCode code) - => CloseAsync(createHandshakeCloseResponse(code)); - - // As server - public async Task ConnectAsServer() - { - try - { - _readyState = WebSocketState.Open; - await OpenAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while connecting.").ConfigureAwait(false); - } - } - - #endregion - - #region Public Methods - - /// - /// Closes the WebSocket connection, and releases all associated resources. - /// - public Task CloseAsync() - { - var msg = _readyState.CheckIfClosable(); - if (msg != null) - { - error(msg); - - return Task.CompletedTask; - } - - var send = _readyState == WebSocketState.Open; - return CloseAsync(new PayloadData(), send, send); - } - - /// - /// Closes the WebSocket connection with the specified - /// and , and releases all associated resources. - /// - /// - /// This method emits a event if the size - /// of is greater than 123 bytes. - /// - /// - /// One of the enum values, represents the status code - /// indicating the reason for the close. - /// - /// - /// A that represents the reason for the close. - /// - public async Task CloseAsync(CloseStatusCode code, string reason) - { - byte[] data = null; - var msg = _readyState.CheckIfClosable() ?? - (data = await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)).CheckIfValidControlData("reason"); - - if (msg != null) - { - error(msg); - - return; - } - - var send = _readyState == WebSocketState.Open && !code.IsReserved(); - await CloseAsync(new PayloadData(data), send, send).ConfigureAwait(false); - } - - /// - /// Sends a binary asynchronously using the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// An array of that represents the binary data to send. - /// - public Task SendAsync(byte[] data) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - var msg = _readyState.CheckIfOpen(); - if (msg != null) - { - throw new Exception(msg); - } - - return SendAsync(Opcode.Binary, new MemoryStream(data)); - } - - /// - /// Sends a text asynchronously using the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// A that represents the text data to send. - /// - public Task SendAsync(string data) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - var msg = _readyState.CheckIfOpen(); - if (msg != null) - { - throw new Exception(msg); - } - - return SendAsync(Opcode.Text, new MemoryStream(Encoding.UTF8.GetBytes(data))); - } - - #endregion - - #region Explicit Interface Implementation - - /// - /// Closes the WebSocket connection, and releases all associated resources. - /// - /// - /// This method closes the WebSocket connection with . - /// - void IDisposable.Dispose() - { - CloseAsync(CloseStatusCode.Away, null).GetAwaiter().GetResult(); - } - - #endregion - } -} diff --git a/SocketHttpListener/WebSocketException.cs b/SocketHttpListener/WebSocketException.cs deleted file mode 100644 index e86c46d0f5..0000000000 --- a/SocketHttpListener/WebSocketException.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; - -namespace SocketHttpListener -{ - /// - /// The exception that is thrown when a gets a fatal error. - /// - public class WebSocketException : Exception - { - #region Internal Constructors - - internal WebSocketException() - : this(CloseStatusCode.Abnormal, null, null) - { - } - - internal WebSocketException(string message) - : this(CloseStatusCode.Abnormal, message, null) - { - } - - internal WebSocketException(CloseStatusCode code) - : this(code, null, null) - { - } - - internal WebSocketException(string message, Exception innerException) - : this(CloseStatusCode.Abnormal, message, innerException) - { - } - - internal WebSocketException(CloseStatusCode code, string message) - : this(code, message, null) - { - } - - internal WebSocketException(CloseStatusCode code, string message, Exception innerException) - : base(message ?? code.GetMessage(), innerException) - { - Code = code; - } - - #endregion - - #region Public Properties - - /// - /// Gets the status code indicating the cause for the exception. - /// - /// - /// One of the enum values, represents the status code indicating - /// the cause for the exception. - /// - public CloseStatusCode Code - { - get; private set; - } - - #endregion - } -} diff --git a/SocketHttpListener/WebSocketFrame.cs b/SocketHttpListener/WebSocketFrame.cs deleted file mode 100644 index 8ec64026bd..0000000000 --- a/SocketHttpListener/WebSocketFrame.cs +++ /dev/null @@ -1,432 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace SocketHttpListener -{ - internal class WebSocketFrame : IEnumerable - { - #region Private Fields - - private byte[] _extPayloadLength; - private Fin _fin; - private Mask _mask; - private byte[] _maskingKey; - private Opcode _opcode; - private PayloadData _payloadData; - private byte _payloadLength; - private Rsv _rsv1; - private Rsv _rsv2; - private Rsv _rsv3; - - #endregion - - #region Internal Fields - - internal static readonly byte[] EmptyUnmaskPingData; - - #endregion - - #region Static Constructor - - static WebSocketFrame() - { - EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray(); - } - - #endregion - - #region Private Constructors - - private WebSocketFrame() - { - } - - #endregion - - #region Internal Constructors - - internal WebSocketFrame(Opcode opcode, PayloadData payload) - : this(Fin.Final, opcode, Mask.Mask, payload, false) - { - } - - internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload) - : this(Fin.Final, opcode, mask, payload, false) - { - } - - internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload) - : this(fin, opcode, mask, payload, false) - { - } - - internal WebSocketFrame( - Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed) - { - _fin = fin; - _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off; - _rsv2 = Rsv.Off; - _rsv3 = Rsv.Off; - _opcode = opcode; - _mask = mask; - - var len = payload.Length; - if (len < 126) - { - _payloadLength = (byte)len; - _extPayloadLength = new byte[0]; - } - else if (len < 0x010000) - { - _payloadLength = (byte)126; - _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big); - } - else - { - _payloadLength = (byte)127; - _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big); - } - - if (mask == Mask.Mask) - { - _maskingKey = createMaskingKey(); - payload.Mask(_maskingKey); - } - else - { - _maskingKey = new byte[0]; - } - - _payloadData = payload; - } - - #endregion - - #region Public Properties - - public byte[] ExtendedPayloadLength => _extPayloadLength; - - public Fin Fin => _fin; - - public bool IsBinary => _opcode == Opcode.Binary; - - public bool IsClose => _opcode == Opcode.Close; - - public bool IsCompressed => _rsv1 == Rsv.On; - - public bool IsContinuation => _opcode == Opcode.Cont; - - public bool IsControl => _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; - - public bool IsData => _opcode == Opcode.Binary || _opcode == Opcode.Text; - - public bool IsFinal => _fin == Fin.Final; - - public bool IsFragmented => _fin == Fin.More || _opcode == Opcode.Cont; - - public bool IsMasked => _mask == Mask.Mask; - - public bool IsPerMessageCompressed => (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On; - - public bool IsPing => _opcode == Opcode.Ping; - - public bool IsPong => _opcode == Opcode.Pong; - - public bool IsText => _opcode == Opcode.Text; - - public ulong Length => 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; - - public Mask Mask => _mask; - - public byte[] MaskingKey => _maskingKey; - - public Opcode Opcode => _opcode; - - public PayloadData PayloadData => _payloadData; - - public byte PayloadLength => _payloadLength; - - public Rsv Rsv1 => _rsv1; - - public Rsv Rsv2 => _rsv2; - - public Rsv Rsv3 => _rsv3; - - #endregion - - #region Private Methods - - private byte[] createMaskingKey() - { - var key = new byte[4]; - var rand = new Random(); - rand.NextBytes(key); - - return key; - } - - private static bool isControl(Opcode opcode) - { - return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; - } - - private static bool isData(Opcode opcode) - { - return opcode == Opcode.Text || opcode == Opcode.Binary; - } - - private static async Task ReadAsync(byte[] header, Stream stream, bool unmask) - { - /* Header */ - - // FIN - var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; - // RSV1 - var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; - // RSV2 - var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; - // RSV3 - var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; - // Opcode - var opcode = (Opcode)(header[0] & 0x0f); - // MASK - var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask; - // Payload Length - var payloadLen = (byte)(header[1] & 0x7f); - - // Check if correct frame. - var incorrect = isControl(opcode) && fin == Fin.More - ? "A control frame is fragmented." - : !isData(opcode) && rsv1 == Rsv.On - ? "A non data frame is compressed." - : null; - - if (incorrect != null) - throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect); - - // Check if consistent frame. - if (isControl(opcode) && payloadLen > 125) - throw new WebSocketException( - CloseStatusCode.InconsistentData, - "The length of payload data of a control frame is greater than 125 bytes."); - - var frame = new WebSocketFrame(); - frame._fin = fin; - frame._rsv1 = rsv1; - frame._rsv2 = rsv2; - frame._rsv3 = rsv3; - frame._opcode = opcode; - frame._mask = mask; - frame._payloadLength = payloadLen; - - /* Extended Payload Length */ - - var size = payloadLen < 126 - ? 0 - : payloadLen == 126 - ? 2 - : 8; - - var extPayloadLen = size > 0 ? await stream.ReadBytesAsync(size).ConfigureAwait(false) : Array.Empty(); - if (size > 0 && extPayloadLen.Length != size) - throw new WebSocketException( - "The 'Extended Payload Length' of a frame cannot be read from the data source."); - - frame._extPayloadLength = extPayloadLen; - - /* Masking Key */ - - var masked = mask == Mask.Mask; - var maskingKey = masked ? await stream.ReadBytesAsync(4).ConfigureAwait(false) : Array.Empty(); - if (masked && maskingKey.Length != 4) - throw new WebSocketException( - "The 'Masking Key' of a frame cannot be read from the data source."); - - frame._maskingKey = maskingKey; - - /* Payload Data */ - - ulong len = payloadLen < 126 - ? payloadLen - : payloadLen == 126 - ? extPayloadLen.ToUInt16(ByteOrder.Big) - : extPayloadLen.ToUInt64(ByteOrder.Big); - - byte[] data = null; - if (len > 0) - { - // Check if allowable payload data length. - if (payloadLen > 126 && len > PayloadData.MaxLength) - throw new WebSocketException( - CloseStatusCode.TooBig, - "The length of 'Payload Data' of a frame is greater than the allowable length."); - - data = payloadLen > 126 - ? await stream.ReadBytesAsync((long)len, 1024).ConfigureAwait(false) - : await stream.ReadBytesAsync((int)len).ConfigureAwait(false); - - //if (data.LongLength != (long)len) - // throw new WebSocketException( - // "The 'Payload Data' of a frame cannot be read from the data source."); - } - else - { - data = Array.Empty(); - } - - var payload = new PayloadData(data, masked); - if (masked && unmask) - { - payload.Mask(maskingKey); - frame._mask = Mask.Unmask; - frame._maskingKey = Array.Empty(); - } - - frame._payloadData = payload; - return frame; - } - - #endregion - - #region Internal Methods - - internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data) - { - return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data)); - } - - internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload) - { - return new WebSocketFrame(Opcode.Close, mask, payload); - } - - internal static async Task CreateCloseFrameAsync(Mask mask, CloseStatusCode code, string reason) - { - return new WebSocketFrame( - Opcode.Close, mask, new PayloadData(await ((ushort)code).AppendAsync(reason).ConfigureAwait(false))); - } - - internal static WebSocketFrame CreatePingFrame(Mask mask) - { - return new WebSocketFrame(Opcode.Ping, mask, new PayloadData()); - } - - internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data) - { - return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data)); - } - - internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload) - { - return new WebSocketFrame(Opcode.Pong, mask, payload); - } - - internal static WebSocketFrame CreateWebSocketFrame( - Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) - { - return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); - } - - internal static Task ReadAsync(Stream stream) - => ReadAsync(stream, true); - - internal static async Task ReadAsync(Stream stream, bool unmask) - { - var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); - if (header.Length != 2) - { - throw new WebSocketException( - "The header part of a frame cannot be read from the data source."); - } - - return await ReadAsync(header, stream, unmask).ConfigureAwait(false); - } - - internal static async Task ReadAsync( - Stream stream, bool unmask, Action completed, Action error) - { - try - { - var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); - if (header.Length != 2) - { - throw new WebSocketException( - "The header part of a frame cannot be read from the data source."); - } - - var frame = await ReadAsync(header, stream, unmask).ConfigureAwait(false); - completed?.Invoke(frame); - } - catch (Exception ex) - { - error.Invoke(ex); - } - } - - #endregion - - #region Public Methods - - public IEnumerator GetEnumerator() - { - foreach (var b in ToByteArray()) - yield return b; - } - - public void Print(bool dumped) - { - //Console.WriteLine(dumped ? dump(this) : print(this)); - } - - public byte[] ToByteArray() - { - using (var buff = new MemoryStream()) - { - var header = (int)_fin; - header = (header << 1) + (int)_rsv1; - header = (header << 1) + (int)_rsv2; - header = (header << 1) + (int)_rsv3; - header = (header << 4) + (int)_opcode; - header = (header << 1) + (int)_mask; - header = (header << 7) + (int)_payloadLength; - buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2); - - if (_payloadLength > 125) - buff.Write(_extPayloadLength, 0, _extPayloadLength.Length); - - if (_mask == Mask.Mask) - buff.Write(_maskingKey, 0, _maskingKey.Length); - - if (_payloadLength > 0) - { - var payload = _payloadData.ToByteArray(); - if (_payloadLength < 127) - buff.Write(payload, 0, payload.Length); - else - buff.WriteBytes(payload); - } - - return buff.ToArray(); - } - } - - public override string ToString() - { - return BitConverter.ToString(ToByteArray()); - } - - #endregion - - #region Explicitly Implemented Interface Members - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - } -} From 25d3d0b731db793e4afc39ff40e0b6a1a27d6ad8 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 08:02:32 +0100 Subject: [PATCH 0093/6208] Remove some unused stuff --- .../SocketSharp/RequestMono.cs | 17 ----------------- .../SocketSharp/WebSocketSharpRequest.cs | 17 ----------------- MediaBrowser.Model/Services/IRequest.cs | 10 ---------- 3 files changed, 44 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index a8142aef68..113f76b10b 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -105,14 +105,6 @@ namespace Emby.Server.Implementations.SocketSharp await LoadWwwForm(form).ConfigureAwait(false); } -#if NET_4_0 - if (validateRequestNewMode && !checked_form) { - // Setting this before calling the validator prevents - // possible endless recursion - checked_form = true; - ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form); - } else -#endif if (validate_form && !checked_form) { checked_form = true; @@ -129,8 +121,6 @@ namespace Emby.Server.Implementations.SocketSharp protected bool validate_cookies { get; set; } protected bool validate_query_string { get; set; } protected bool validate_form { get; set; } - protected bool checked_cookies { get; set; } - protected bool checked_query_string { get; set; } protected bool checked_form { get; set; } private static void ThrowValidationException(string name, string key, string value) @@ -210,13 +200,6 @@ namespace Emby.Server.Implementations.SocketSharp return false; } - public void ValidateInput() - { - validate_cookies = true; - validate_query_string = true; - validate_form = true; - } - private bool IsContentType(string ct, bool starts_with) { if (ct == null || ContentType == null) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 53dce667ba..bddccf68ba 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -48,8 +48,6 @@ namespace Emby.Server.Implementations.SocketSharp public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); - public string UserHostAddress => request.HttpContext.Connection.RemoteIpAddress.ToString(); - public string XForwardedFor => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); @@ -142,19 +140,6 @@ namespace Emby.Server.Implementations.SocketSharp return name; } - internal static bool ContainsNonAsciiChars(string token) - { - for (int i = 0; i < token.Length; ++i) - { - if ((token[i] < 0x20) || (token[i] > 0x7e)) - { - return true; - } - } - - return false; - } - private string NormalizeIp(string ip) { if (!string.IsNullOrWhiteSpace(ip)) @@ -171,8 +156,6 @@ namespace Emby.Server.Implementations.SocketSharp return ip; } - public bool IsSecureConnection => request.IsHttps || XForwardedProtocol == "https"; - public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index c7dccdb6fd..0fd4ea37b1 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -61,11 +61,6 @@ namespace MediaBrowser.Model.Services string AbsoluteUri { get; } - /// - /// The Remote Ip as reported by Request.UserHostAddress - /// - string UserHostAddress { get; } - /// /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress /// @@ -76,11 +71,6 @@ namespace MediaBrowser.Model.Services /// string Authorization { get; } - /// - /// e.g. is https or not - /// - bool IsSecureConnection { get; } - string[] AcceptTypes { get; } string PathInfo { get; } From 333bd2107a05c656b8fe27364dcd05fc7ca249d5 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 12:40:18 +0100 Subject: [PATCH 0094/6208] Remove HttpUtility --- Emby.Dlna/PlayTo/PlayToController.cs | 22 +- .../HttpServer/HttpListenerHost.cs | 3 +- MediaBrowser.Model/Services/HttpUtility.cs | 691 ------------------ 3 files changed, 14 insertions(+), 702 deletions(-) delete mode 100644 MediaBrowser.Model/Services/HttpUtility.cs diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index be86dde16a..be6810d0e9 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -19,7 +19,9 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Emby.Dlna.PlayTo { @@ -847,13 +849,13 @@ namespace Emby.Dlna.PlayTo if (index == -1) return request; var query = url.Substring(index + 1); - QueryParamCollection values = MyHttpUtility.ParseQueryString(query); + var values = QueryHelpers.ParseQuery(query); - request.DeviceProfileId = values.Get("DeviceProfileId"); - request.DeviceId = values.Get("DeviceId"); - request.MediaSourceId = values.Get("MediaSourceId"); - request.LiveStreamId = values.Get("LiveStreamId"); - request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase); + request.DeviceProfileId = values["DeviceProfileId"].ToString(); + request.DeviceId = values["DeviceId"].ToString(); + request.MediaSourceId = values["MediaSourceId"].ToString(); + request.LiveStreamId = values["LiveStreamId"].ToString(); + request.IsDirectStream = string.Equals("true", values["Static"].ToString(), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); @@ -867,9 +869,9 @@ namespace Emby.Dlna.PlayTo } } - private static int? GetIntValue(QueryParamCollection values, string name) + private static int? GetIntValue(IReadOnlyDictionary values, string name) { - var value = values.Get(name); + var value = values[name].ToString(); if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { @@ -879,9 +881,9 @@ namespace Emby.Dlna.PlayTo return null; } - private static long GetLongValue(QueryParamCollection values, string name) + private static long GetLongValue(IReadOnlyDictionary values, string name) { - var value = values.Get(name); + var value = values[name].ToString(); if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 5b1cb433a9..2abc6c2f4f 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -22,6 +22,7 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ServiceStack.Text.Jsv; @@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.HttpServer var uri = new Uri(url); // this gets all the query string key value pairs as a collection - var newQueryString = MyHttpUtility.ParseQueryString(uri.Query); + var newQueryString = QueryHelpers.ParseQuery(uri.Query); var originalCount = newQueryString.Count; diff --git a/MediaBrowser.Model/Services/HttpUtility.cs b/MediaBrowser.Model/Services/HttpUtility.cs deleted file mode 100644 index be180334c6..0000000000 --- a/MediaBrowser.Model/Services/HttpUtility.cs +++ /dev/null @@ -1,691 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace MediaBrowser.Model.Services -{ - public static class MyHttpUtility - { - // Must be sorted - static readonly long[] entities = new long[] { - (long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'A' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'A' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'B' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'C' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'C' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'D' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'D' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'E' << 56 | (long)'T' << 48 | (long)'H' << 40, - (long)'E' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'E' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'E' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'E' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'G' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'I' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'I' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'I' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'K' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'L' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'M' << 56 | (long)'u' << 48, - (long)'N' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'N' << 56 | (long)'u' << 48, - (long)'O' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'O' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'O' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'O' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'O' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'O' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'P' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'P' << 56 | (long)'i' << 48, - (long)'P' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'P' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'R' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'S' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'S' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'T' << 56 | (long)'H' << 48 | (long)'O' << 40 | (long)'R' << 32 | (long)'N' << 24, - (long)'T' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'T' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'U' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'U' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'U' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'X' << 56 | (long)'i' << 48, - (long)'Y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'Y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'Z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'a' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'a' << 56 | (long)'c' << 48 | (long)'u' << 40 | (long)'t' << 32 | (long)'e' << 24, - (long)'a' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'l' << 48 | (long)'e' << 40 | (long)'f' << 32 | (long)'s' << 24 | (long)'y' << 16 | (long)'m' << 8, - (long)'a' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'a' << 56 | (long)'m' << 48 | (long)'p' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'d' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'g' << 40, - (long)'a' << 56 | (long)'p' << 48 | (long)'o' << 40 | (long)'s' << 32, - (long)'a' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'s' << 48 | (long)'y' << 40 | (long)'m' << 32 | (long)'p' << 24, - (long)'a' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'b' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'b' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'b' << 56 | (long)'r' << 48 | (long)'v' << 40 | (long)'b' << 32 | (long)'a' << 24 | (long)'r' << 16, - (long)'b' << 56 | (long)'u' << 48 | (long)'l' << 40 | (long)'l' << 32, - (long)'c' << 56 | (long)'a' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'c' << 56 | (long)'e' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'c' << 56 | (long)'e' << 48 | (long)'n' << 40 | (long)'t' << 32, - (long)'c' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'c' << 56 | (long)'i' << 48 | (long)'r' << 40 | (long)'c' << 32, - (long)'c' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'b' << 32 | (long)'s' << 24, - (long)'c' << 56 | (long)'o' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'c' << 56 | (long)'o' << 48 | (long)'p' << 40 | (long)'y' << 32, - (long)'c' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'r' << 24, - (long)'c' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'n' << 16, - (long)'d' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'d' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'d' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'a' << 40 | (long)'m' << 32 | (long)'s' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'v' << 40 | (long)'i' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'e' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'m' << 48 | (long)'p' << 40 | (long)'t' << 32 | (long)'y' << 24, - (long)'e' << 56 | (long)'m' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'n' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'e' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'i' << 32 | (long)'v' << 24, - (long)'e' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'e' << 56 | (long)'t' << 48 | (long)'h' << 40, - (long)'e' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'e' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'o' << 32, - (long)'e' << 56 | (long)'x' << 48 | (long)'i' << 40 | (long)'s' << 32 | (long)'t' << 24, - (long)'f' << 56 | (long)'n' << 48 | (long)'o' << 40 | (long)'f' << 32, - (long)'f' << 56 | (long)'o' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'l' << 24 | (long)'l' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'2' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'3' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'l' << 24, - (long)'g' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'g' << 56 | (long)'e' << 48, - (long)'g' << 56 | (long)'t' << 48, - (long)'h' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'t' << 24 | (long)'s' << 16, - (long)'h' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'l' << 32 | (long)'i' << 24 | (long)'p' << 16, - (long)'i' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'i' << 56 | (long)'e' << 48 | (long)'x' << 40 | (long)'c' << 32 | (long)'l' << 24, - (long)'i' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'m' << 48 | (long)'a' << 40 | (long)'g' << 32 | (long)'e' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'f' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'t' << 40, - (long)'i' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'i' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'e' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'i' << 56 | (long)'s' << 48 | (long)'i' << 40 | (long)'n' << 32, - (long)'i' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'k' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'l' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'l' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'l' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'e' << 48, - (long)'l' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'w' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'z' << 40, - (long)'l' << 56 | (long)'r' << 48 | (long)'m' << 40, - (long)'l' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'l' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'t' << 48, - (long)'m' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'r' << 32, - (long)'m' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'c' << 40 | (long)'r' << 32 | (long)'o' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'d' << 40 | (long)'d' << 32 | (long)'o' << 24 | (long)'t' << 16, - (long)'m' << 56 | (long)'i' << 48 | (long)'n' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'m' << 56 | (long)'u' << 48, - (long)'n' << 56 | (long)'a' << 48 | (long)'b' << 40 | (long)'l' << 32 | (long)'a' << 24, - (long)'n' << 56 | (long)'b' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'n' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'n' << 56 | (long)'e' << 48, - (long)'n' << 56 | (long)'i' << 48, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'n' << 56 | (long)'s' << 48 | (long)'u' << 40 | (long)'b' << 32, - (long)'n' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'n' << 56 | (long)'u' << 48, - (long)'o' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'o' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'o' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'l' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'e' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'o' << 56 | (long)'p' << 48 | (long)'l' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'o' << 56 | (long)'r' << 48, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'f' << 32, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'m' << 32, - (long)'o' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'o' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'a' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'t' << 32, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'m' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'p' << 56 | (long)'i' << 48, - (long)'p' << 56 | (long)'i' << 48 | (long)'v' << 40, - (long)'p' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'s' << 32 | (long)'m' << 24 | (long)'n' << 16, - (long)'p' << 56 | (long)'o' << 48 | (long)'u' << 40 | (long)'n' << 32 | (long)'d' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'d' << 32, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'q' << 56 | (long)'u' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'r' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'c' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'r' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'l' << 32, - (long)'r' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'r' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'r' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'r' << 56 | (long)'l' << 48 | (long)'m' << 40, - (long)'r' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'r' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'b' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'s' << 56 | (long)'d' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'e' << 48 | (long)'c' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'h' << 48 | (long)'y' << 40, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24 | (long)'f' << 16, - (long)'s' << 56 | (long)'i' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'p' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'1' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'2' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'3' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'z' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'t' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'4' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24 | (long)'s' << 16 | (long)'y' << 8 | (long)'m' << 0, - (long)'t' << 56 | (long)'h' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'s' << 24 | (long)'p' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'o' << 40 | (long)'r' << 32 | (long)'n' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'l' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'m' << 40 | (long)'e' << 32 | (long)'s' << 24, - (long)'t' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'u' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'u' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'m' << 48 | (long)'l' << 40, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'h' << 24, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'u' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'w' << 56 | (long)'e' << 48 | (long)'i' << 40 | (long)'e' << 32 | (long)'r' << 24 | (long)'p' << 16, - (long)'x' << 56 | (long)'i' << 48, - (long)'y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'y' << 56 | (long)'e' << 48 | (long)'n' << 40, - (long)'y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'z' << 56 | (long)'w' << 48 | (long)'j' << 40, - (long)'z' << 56 | (long)'w' << 48 | (long)'n' << 40 | (long)'j' << 32 - }; - - static readonly char[] entities_values = new char[] { - '\u00C6', '\u00C1', '\u00C2', '\u00C0', '\u0391', '\u00C5', '\u00C3', '\u00C4', '\u0392', '\u00C7', '\u03A7', - '\u2021', '\u0394', '\u00D0', '\u00C9', '\u00CA', '\u00C8', '\u0395', '\u0397', '\u00CB', '\u0393', '\u00CD', - '\u00CE', '\u00CC', '\u0399', '\u00CF', '\u039A', '\u039B', '\u039C', '\u00D1', '\u039D', '\u0152', '\u00D3', - '\u00D4', '\u00D2', '\u03A9', '\u039F', '\u00D8', '\u00D5', '\u00D6', '\u03A6', '\u03A0', '\u2033', '\u03A8', - '\u03A1', '\u0160', '\u03A3', '\u00DE', '\u03A4', '\u0398', '\u00DA', '\u00DB', '\u00D9', '\u03A5', '\u00DC', - '\u039E', '\u00DD', '\u0178', '\u0396', '\u00E1', '\u00E2', '\u00B4', '\u00E6', '\u00E0', '\u2135', '\u03B1', - '\u0026', '\u2227', '\u2220', '\u0027', '\u00E5', '\u2248', '\u00E3', '\u00E4', '\u201E', '\u03B2', '\u00A6', - '\u2022', '\u2229', '\u00E7', '\u00B8', '\u00A2', '\u03C7', '\u02C6', '\u2663', '\u2245', '\u00A9', '\u21B5', - '\u222A', '\u00A4', '\u21D3', '\u2020', '\u2193', '\u00B0', '\u03B4', '\u2666', '\u00F7', '\u00E9', '\u00EA', - '\u00E8', '\u2205', '\u2003', '\u2002', '\u03B5', '\u2261', '\u03B7', '\u00F0', '\u00EB', '\u20AC', '\u2203', - '\u0192', '\u2200', '\u00BD', '\u00BC', '\u00BE', '\u2044', '\u03B3', '\u2265', '\u003E', '\u21D4', '\u2194', - '\u2665', '\u2026', '\u00ED', '\u00EE', '\u00A1', '\u00EC', '\u2111', '\u221E', '\u222B', '\u03B9', '\u00BF', - '\u2208', '\u00EF', '\u03BA', '\u21D0', '\u03BB', '\u2329', '\u00AB', '\u2190', '\u2308', '\u201C', '\u2264', - '\u230A', '\u2217', '\u25CA', '\u200E', '\u2039', '\u2018', '\u003C', '\u00AF', '\u2014', '\u00B5', '\u00B7', - '\u2212', '\u03BC', '\u2207', '\u00A0', '\u2013', '\u2260', '\u220B', '\u00AC', '\u2209', '\u2284', '\u00F1', - '\u03BD', '\u00F3', '\u00F4', '\u0153', '\u00F2', '\u203E', '\u03C9', '\u03BF', '\u2295', '\u2228', '\u00AA', - '\u00BA', '\u00F8', '\u00F5', '\u2297', '\u00F6', '\u00B6', '\u2202', '\u2030', '\u22A5', '\u03C6', '\u03C0', - '\u03D6', '\u00B1', '\u00A3', '\u2032', '\u220F', '\u221D', '\u03C8', '\u0022', '\u21D2', '\u221A', '\u232A', - '\u00BB', '\u2192', '\u2309', '\u201D', '\u211C', '\u00AE', '\u230B', '\u03C1', '\u200F', '\u203A', '\u2019', - '\u201A', '\u0161', '\u22C5', '\u00A7', '\u00AD', '\u03C3', '\u03C2', '\u223C', '\u2660', '\u2282', '\u2286', - '\u2211', '\u2283', '\u00B9', '\u00B2', '\u00B3', '\u2287', '\u00DF', '\u03C4', '\u2234', '\u03B8', '\u03D1', - '\u2009', '\u00FE', '\u02DC', '\u00D7', '\u2122', '\u21D1', '\u00FA', '\u2191', '\u00FB', '\u00F9', '\u00A8', - '\u03D2', '\u03C5', '\u00FC', '\u2118', '\u03BE', '\u00FD', '\u00A5', '\u00FF', '\u03B6', '\u200D', '\u200C' - }; - - #region Methods - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf, 0, buf.Length); - - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - static bool TryConvertKeyToEntity(string key, out char value) - { - var token = CalculateKeyValue(key); - if (token == 0) - { - value = '\0'; - return false; - } - - var idx = Array.BinarySearch(entities, token); - if (idx < 0) - { - value = '\0'; - return false; - } - - value = entities_values[idx]; - return true; - } - - static long CalculateKeyValue(string s) - { - if (s.Length > 8) - return 0; - - long key = 0; - for (int i = 0; i < s.Length; ++i) - { - long ch = s[i]; - if (ch > 'z' || ch < '0') - return 0; - - key |= ch << ((7 - i) * 8); - } - - return key; - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - - if (s.IndexOf('&') == -1) - return s; - - var entity = new StringBuilder(); - var output = new StringBuilder(); - int len = s.Length; - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - int state = 0; - int number = 0; - int digit_start = 0; - bool hex_number = false; - - for (int i = 0; i < len; i++) - { - char c = s[i]; - if (state == 0) - { - if (c == '&') - { - entity.Append(c); - state = 1; - } - else - { - output.Append(c); - } - continue; - } - - if (c == '&') - { - state = 1; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - output.Append(entity.ToString()); - entity.Length = 0; - entity.Append('&'); - continue; - } - - switch (state) - { - case 1: - if (c == ';') - { - state = 0; - output.Append(entity.ToString()); - output.Append(c); - entity.Length = 0; - break; - } - - number = 0; - hex_number = false; - if (c != '#') - { - state = 2; - } - else - { - state = 3; - } - entity.Append(c); - - break; - case 2: - entity.Append(c); - if (c == ';') - { - string key = entity.ToString(); - state = 0; - entity.Length = 0; - - if (key.Length > 1) - { - var skey = key.Substring(1, key.Length - 2); - if (TryConvertKeyToEntity(skey, out c)) - { - output.Append(c); - break; - } - } - - output.Append(key); - } - - break; - case 3: - if (c == ';') - { - if (number < 0x10000) - { - output.Append((char)number); - } - else - { - output.Append((char)(0xd800 + ((number - 0x10000) >> 10))); - output.Append((char)(0xdc00 + ((number - 0x10000) & 0x3ff))); - } - state = 0; - entity.Length = 0; - digit_start = 0; - break; - } - - if (c == 'x' || c == 'X' && !hex_number) - { - digit_start = i; - hex_number = true; - break; - } - - if (char.IsDigit(c)) - { - if (digit_start == 0) - digit_start = i; - - number = number * (hex_number ? 16 : 10) + ((int)c - '0'); - break; - } - - if (hex_number) - { - if (c >= 'a' && c <= 'f') - { - number = number * 16 + 10 + ((int)c - 'a'); - break; - } - if (c >= 'A' && c <= 'F') - { - number = number * 16 + 10 + ((int)c - 'A'); - break; - } - } - - state = 2; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - entity.Append(c); - break; - } - } - - if (entity.Length > 0) - { - output.Append(entity); - } - else if (digit_start > 0) - { - output.Append(s, digit_start, s.Length - digit_start); - } - return output.ToString(); - } - - public static QueryParamCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static QueryParamCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException(nameof(query)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new QueryParamCollection(); - if (query[0] == '?') - query = query.Substring(1); - - var result = new QueryParamCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} From 91afaaf8fe43035bb4832da44b6d6741d2815fb5 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 12:45:06 +0100 Subject: [PATCH 0095/6208] Cleanup in QueryParamCollection --- Emby.Dlna/PlayTo/PlayToController.cs | 2 +- .../Services/QueryParamCollection.cs | 59 +------------------ 2 files changed, 2 insertions(+), 59 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index be6810d0e9..2da8bb8609 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -849,7 +849,7 @@ namespace Emby.Dlna.PlayTo if (index == -1) return request; var query = url.Substring(index + 1); - var values = QueryHelpers.ParseQuery(query); + Dictionary values = QueryHelpers.ParseQuery(query); request.DeviceProfileId = values["DeviceProfileId"].ToString(); request.DeviceId = values["DeviceId"].ToString(); diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 09806ed9c2..9f23b2420f 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Services } - public QueryParamCollection(IDictionary headers) + public QueryParamCollection(IHeaderDictionary headers) { foreach (var pair in headers) { @@ -24,38 +24,6 @@ namespace MediaBrowser.Model.Services } } - public QueryParamCollection(Microsoft.AspNetCore.Http.IHeaderDictionary headers) - { - foreach (var pair in headers) - { - Add(pair.Key, pair.Value); - } - } - - // TODO remove this shit - public QueryParamCollection(WebHeaderCollection webHeaderCollection) - { - foreach (var key in webHeaderCollection.AllKeys) - { - foreach (var value in webHeaderCollection.GetValues(key) ?? Array.Empty()) - { - Add(key, value); - } - } - } - - // TODO remove this shit - public QueryParamCollection(NameValueCollection nameValueCollection) - { - foreach (var key in nameValueCollection.AllKeys) - { - foreach (var value in nameValueCollection.GetValues(key) ?? Array.Empty()) - { - Add(key, value); - } - } - } - public QueryParamCollection(IQueryCollection queryCollection) { foreach (var pair in queryCollection) @@ -74,21 +42,6 @@ namespace MediaBrowser.Model.Services return StringComparer.OrdinalIgnoreCase; } - public string GetKey(int index) - { - return this[index].Name; - } - - public string Get(int index) - { - return this[index].Value; - } - - public virtual string[] GetValues(int index) - { - return new[] { Get(index) }; - } - /// /// Adds a new query parameter. /// @@ -129,16 +82,6 @@ namespace MediaBrowser.Model.Services Add(key, value); } - /// - /// Removes all parameters of the given name. - /// - /// The number of parameters that were removed - /// is null. - public virtual int Remove(string name) - { - return RemoveAll(p => p.Name == name); - } - public string Get(string name) { var stringComparison = GetStringComparison(); From 7429c07c05ad12fbacef7952574edd75c294eb8a Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Wed, 27 Feb 2019 20:16:54 +0800 Subject: [PATCH 0096/6208] Remove redundant parenthesis --- Emby.Server.Implementations/Networking/NetworkManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 8696d18968..ace93ebdee 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking // Try to exclude virtual adapters // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms var addr = ipProperties.GatewayAddresses.FirstOrDefault(); - if (addr == null || (ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))) + if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase)) { return new List(); } From 27e7e792b3d95912787c613f849548809d48f6b1 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 14:23:39 +0100 Subject: [PATCH 0097/6208] Replace some usage of QueryParamCollection --- Emby.Dlna/Api/DlnaServerService.cs | 10 +-- .../ConnectionManager/ConnectionManager.cs | 3 +- .../ContentDirectory/ContentDirectory.cs | 2 +- Emby.Dlna/ControlRequest.cs | 5 +- Emby.Dlna/DlnaManager.cs | 17 ++-- Emby.Dlna/IUpnpService.cs | 5 +- .../MediaReceiverRegistrar.cs | 3 +- .../HttpServer/HttpListenerHost.cs | 7 +- .../HttpServer/HttpResultFactory.cs | 19 ++--- .../HttpServer/WebSocketConnection.cs | 3 +- .../Net/WebSocketConnectEventArgs.cs | 3 +- .../Services/ServiceHandler.cs | 2 +- .../Session/SessionWebSocketListener.cs | 3 +- .../SocketSharp/RequestMono.cs | 2 - .../SocketSharp/WebSocketSharpListener.cs | 6 +- .../SocketSharp/WebSocketSharpRequest.cs | 12 +-- .../SocketSharp/WebSocketSharpResponse.cs | 83 +------------------ .../Playback/BaseStreamingService.cs | 6 +- MediaBrowser.Controller/Dlna/IDlnaManager.cs | 5 +- .../Net/IWebSocketConnection.cs | 3 +- .../Net/WebSocketConnectEventArgs.cs | 6 +- MediaBrowser.Model/Services/IHttpRequest.cs | 5 -- MediaBrowser.Model/Services/IHttpResponse.cs | 12 --- MediaBrowser.Model/Services/IRequest.cs | 21 ++--- .../Services/QueryParamCollection.cs | 20 ----- 25 files changed, 66 insertions(+), 197 deletions(-) diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index 68bf801637..8bf3797f85 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -136,7 +136,7 @@ namespace Emby.Dlna.Api { var url = Request.AbsoluteUri; var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers.ToDictionary(), request.UuId, serverAddress); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress); var cacheLength = TimeSpan.FromDays(1); var cacheKey = Request.RawUrl.GetMD5(); @@ -147,21 +147,21 @@ namespace Emby.Dlna.Api public object Get(GetContentDirectory request) { - var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary()); + var xml = ContentDirectory.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } public object Get(GetMediaReceiverRegistrar request) { - var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary()); + var xml = MediaReceiverRegistrar.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } public object Get(GetConnnectionManager request) { - var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary()); + var xml = ConnectionManager.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } @@ -193,7 +193,7 @@ namespace Emby.Dlna.Api return service.ProcessControlRequest(new ControlRequest { - Headers = Request.Headers.ToDictionary(), + Headers = Request.Headers, InputXml = requestStream, TargetServerUuId = id, RequestedUrl = Request.AbsoluteUri diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs index cc427f2a15..e138b91d6a 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -24,7 +23,7 @@ namespace Emby.Dlna.ConnectionManager XmlReaderSettingsFactory = xmlReaderSettingsFactory; } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new ConnectionManagerXmlBuilder().GetXml(); } diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs index b0fec90e69..867e6112fd 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -65,7 +65,7 @@ namespace Emby.Dlna.ContentDirectory } } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new ContentDirectoryXmlBuilder().GetXml(); } diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index afd9a0b874..907d437f85 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using System.IO; +using Microsoft.AspNetCore.Http; namespace Emby.Dlna { public class ControlRequest { - public IDictionary Headers { get; set; } + public IHeaderDictionary Headers { get; set; } public Stream InputXml { get; set; } @@ -15,7 +16,7 @@ namespace Emby.Dlna public ControlRequest() { - Headers = new Dictionary(); + Headers = new HeaderDictionary(); } } } diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index f53d274516..770a901521 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -17,7 +17,9 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.IO; using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Emby.Dlna { @@ -205,16 +207,13 @@ namespace Emby.Dlna } } - public DeviceProfile GetProfile(IDictionary headers) + public DeviceProfile GetProfile(IHeaderDictionary headers) { if (headers == null) { throw new ArgumentNullException(nameof(headers)); } - // Convert to case insensitive - headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); - var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); if (profile != null) @@ -230,12 +229,12 @@ namespace Emby.Dlna return profile; } - private bool IsMatch(IDictionary headers, DeviceIdentification profileInfo) + private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo) { return profileInfo.Headers.Any(i => IsMatch(headers, i)); } - private bool IsMatch(IDictionary headers, HttpHeaderInfo header) + private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header) { // Handle invalid user setup if (string.IsNullOrEmpty(header.Name)) @@ -243,14 +242,14 @@ namespace Emby.Dlna return false; } - if (headers.TryGetValue(header.Name, out string value)) + if (headers.TryGetValue(header.Name, out StringValues value)) { switch (header.Match) { case HeaderMatchType.Equals: return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); case HeaderMatchType.Substring: - var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; + var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); return isMatch; case HeaderMatchType.Regex: @@ -493,7 +492,7 @@ namespace Emby.Dlna internal string Path { get; set; } } - public string GetServerDescriptionXml(IDictionary headers, string serverUuId, string serverAddress) + public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) { var profile = GetProfile(headers) ?? GetDefaultProfile(); diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs index ab8aa46192..ae90e95c79 100644 --- a/Emby.Dlna/IUpnpService.cs +++ b/Emby.Dlna/IUpnpService.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Emby.Dlna { public interface IUpnpService @@ -7,9 +5,8 @@ namespace Emby.Dlna /// /// Gets the content directory XML. /// - /// The headers. /// System.String. - string GetServiceXml(IDictionary headers); + string GetServiceXml(); /// /// Processes the control request. diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index 2b84528eab..9c6022b6cb 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -3,6 +3,7 @@ using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Xml; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar @@ -19,7 +20,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar XmlReaderSettingsFactory = xmlReaderSettingsFactory; } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new MediaReceiverRegistrarXmlBuilder().GetXml(); } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 2abc6c2f4f..70753e5634 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -22,6 +22,7 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -154,7 +155,7 @@ namespace Emby.Server.Implementations.HttpServer { OnReceive = ProcessWebSocketMessageReceived, Url = e.Url, - QueryString = e.QueryString ?? new QueryParamCollection() + QueryString = e.QueryString ?? new QueryCollection() }; connection.Closed += Connection_Closed; @@ -606,8 +607,8 @@ namespace Emby.Server.Implementations.HttpServer } finally { - httpRes.Close(); - + // TODO + httpRes.IsClosed = true; stopWatch.Stop(); var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 09cdbc3c26..52c8221f68 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -16,6 +16,8 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using IRequest = MediaBrowser.Model.Services.IRequest; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; @@ -246,9 +248,9 @@ namespace Emby.Server.Implementations.HttpServer private static string GetCompressionType(IRequest request) { - var acceptEncoding = request.Headers["Accept-Encoding"]; + var acceptEncoding = request.Headers["Accept-Encoding"].ToString(); - if (acceptEncoding != null) + if (string.IsNullOrEmpty(acceptEncoding)) { //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) // return "br"; @@ -424,12 +426,12 @@ namespace Emby.Server.Implementations.HttpServer /// private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options) { - bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; + bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); if (!noCache) { - DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader); + DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) { @@ -530,7 +532,7 @@ namespace Emby.Server.Implementations.HttpServer options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); var contentType = options.ContentType; - if (!string.IsNullOrEmpty(requestContext.Headers.Get("If-Modified-Since"))) + if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) { // See if the result is already cached in the browser var result = GetCachedResult(requestContext, options.ResponseHeaders, options); @@ -548,7 +550,7 @@ namespace Emby.Server.Implementations.HttpServer AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); AddAgeHeader(responseHeaders, options.DateLastModified); - var rangeHeader = requestContext.Headers.Get("Range"); + var rangeHeader = requestContext.Headers["Range"]; if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) { @@ -609,11 +611,6 @@ namespace Emby.Server.Implementations.HttpServer } } - /// - /// The us culture - /// - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// /// Adds the caching responseHeaders. /// diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index e9d0bac74d..2bf460bd11 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using UtfUnknown; @@ -67,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer /// Gets or sets the query string. /// /// The query string. - public QueryParamCollection QueryString { get; set; } + public IQueryCollection QueryString { get; set; } /// /// Initializes a new instance of the class. diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs index 666f1f6011..e3047d3926 100644 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs @@ -1,6 +1,7 @@ using System; using System.Net.WebSockets; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.Net { @@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Net /// Gets or sets the query string. /// /// The query string. - public QueryParamCollection QueryString { get; set; } + public IQueryCollection QueryString { get; set; } /// /// Gets or sets the web socket. /// diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 7e836e22c3..3c8adfc983 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -154,7 +154,7 @@ namespace Emby.Server.Implementations.Services { if (name == null) continue; //thank you ASP.NET - var values = request.QueryString.GetValues(name); + var values = request.QueryString[name]; if (values.Count == 1) { map[name] = values[0]; diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 24903f5e8c..a551433ed9 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Session } } - private SessionInfo GetSession(QueryParamCollection queryString, string remoteEndpoint) + private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint) { if (queryString == null) { diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index 113f76b10b..f73adc5ff8 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -118,8 +118,6 @@ namespace Emby.Server.Implementations.SocketSharp public string Authorization => StringValues.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"].ToString(); - protected bool validate_cookies { get; set; } - protected bool validate_query_string { get; set; } protected bool validate_form { get; set; } protected bool checked_form { get; set; } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 77469244b9..9f046c3fd0 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -52,12 +52,10 @@ using Microsoft.Extensions.Logging; var endpoint = ctx.Connection.RemoteIpAddress.ToString(); var url = ctx.Request.GetDisplayUrl(); - var queryString = new QueryParamCollection(ctx.Request.Query); - var connectingArgs = new WebSocketConnectingEventArgs { Url = url, - QueryString = queryString, + QueryString = ctx.Request.Query, Endpoint = endpoint }; @@ -73,7 +71,7 @@ using Microsoft.Extensions.Logging; WebSocketConnected(new WebSocketConnectEventArgs { Url = url, - QueryString = queryString, + QueryString = ctx.Request.Query, WebSocket = socket, Endpoint = endpoint }); diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index bddccf68ba..24fd36062f 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; using IResponse = MediaBrowser.Model.Services.IResponse; namespace Emby.Server.Implementations.SocketSharp @@ -21,7 +20,7 @@ namespace Emby.Server.Implementations.SocketSharp public partial class WebSocketSharpRequest : IHttpRequest { private readonly HttpRequest request; - private readonly IHttpResponse response; + private readonly IResponse response; public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger) { @@ -34,11 +33,9 @@ namespace Emby.Server.Implementations.SocketSharp public HttpRequest HttpRequest => request; - public object OriginalRequest => request; - public IResponse Response => response; - public IHttpResponse HttpResponse => response; + public IResponse HttpResponse => response; public string OperationName { get; set; } @@ -396,10 +393,9 @@ namespace Emby.Server.Implementations.SocketSharp public string UserAgent => request.Headers[HeaderNames.UserAgent]; - public QueryParamCollection Headers => new QueryParamCollection(request.Headers); + public IHeaderDictionary Headers => request.Headers; - private QueryParamCollection queryString; - public QueryParamCollection QueryString => queryString ?? (queryString = new QueryParamCollection(request.Query)); + public IQueryCollection QueryString => request.Query; public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString()); diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs index f9ecb52a55..c4fbaddd3e 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -1,23 +1,18 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; -using System.Net.Sockets; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; using IRequest = MediaBrowser.Model.Services.IRequest; namespace Emby.Server.Implementations.SocketSharp { - public class WebSocketSharpResponse : IHttpResponse + public class WebSocketSharpResponse : IResponse { private readonly ILogger _logger; @@ -51,42 +46,7 @@ namespace Emby.Server.Implementations.SocketSharp set => _response.ContentType = value; } - public QueryParamCollection Headers => new QueryParamCollection(_response.Headers); - - private static string AsHeaderValue(Cookie cookie) - { - DateTime defaultExpires = DateTime.MinValue; - - var path = cookie.Expires == defaultExpires - ? "/" - : cookie.Path ?? "/"; - - var sb = new StringBuilder(); - - sb.Append($"{cookie.Name}={cookie.Value};path={path}"); - - if (cookie.Expires != defaultExpires) - { - sb.Append($";expires={cookie.Expires:R}"); - } - - if (!string.IsNullOrEmpty(cookie.Domain)) - { - sb.Append($";domain={cookie.Domain}"); - } - - if (cookie.Secure) - { - sb.Append(";Secure"); - } - - if (cookie.HttpOnly) - { - sb.Append(";HttpOnly"); - } - - return sb.ToString(); - } + public IHeaderDictionary Headers => _response.Headers; public void AddHeader(string name, string value) { @@ -111,51 +71,14 @@ namespace Emby.Server.Implementations.SocketSharp public Stream OutputStream => _response.Body; - public void Close() - { - if (!this.IsClosed) - { - this.IsClosed = true; - - try - { - var response = this._response; - - var outputStream = response.Body; - - // This is needed with compression - outputStream.Flush(); - outputStream.Dispose(); - } - catch (SocketException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in HttpListenerResponseWrapper"); - } - } - } - public bool IsClosed { get; - private set; - } - - public void SetCookie(Cookie cookie) - { - var cookieStr = AsHeaderValue(cookie); - _response.Headers.Add("Set-Cookie", cookieStr); + set; } public bool SendChunked { get; set; } - public bool KeepAlive { get; set; } - - public void ClearCookies() - { - } const int StreamCopyToBufferSize = 81920; public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index a6be071b84..ae259a4f59 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -609,12 +609,12 @@ namespace MediaBrowser.Api.Playback { foreach (var param in Request.QueryString) { - if (char.IsLower(param.Name[0])) + if (char.IsLower(param.Key[0])) { // This was probably not parsed initially and should be a StreamOptions // TODO: This should be incorporated either in the lower framework for parsing requests // or the generated URL should correctly serialize it - request.StreamOptions[param.Name] = param.Value; + request.StreamOptions[param.Key] = param.Value; } } } @@ -867,7 +867,7 @@ namespace MediaBrowser.Api.Playback private void ApplyDeviceProfileSettings(StreamState state) { - var headers = Request.Headers.ToDictionary(); + var headers = Request.Headers; if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId)) { diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index a6ee7c5050..41a7686a37 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Dlna { @@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Dlna /// /// The headers. /// DeviceProfile. - DeviceProfile GetProfile(IDictionary headers); + DeviceProfile GetProfile(IHeaderDictionary headers); /// /// Gets the default profile. @@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Dlna /// The server uu identifier. /// The server address. /// System.String. - string GetServerDescriptionXml(IDictionary headers, string serverUuId, string serverAddress); + string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress); /// /// Gets the icon. diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index a09b2f7a22..566897b31f 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -35,7 +36,7 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the query string. /// /// The query string. - QueryParamCollection QueryString { get; set; } + IQueryCollection QueryString { get; set; } /// /// Gets or sets the receive action. diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs index f26b764bb3..107e674216 100644 --- a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs +++ b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs @@ -1,5 +1,7 @@ using System; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; namespace MediaBrowser.Controller.Net { @@ -22,7 +24,7 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the query string. /// /// The query string. - public QueryParamCollection QueryString { get; set; } + public IQueryCollection QueryString { get; set; } /// /// Gets or sets a value indicating whether [allow connection]. /// @@ -31,7 +33,7 @@ namespace MediaBrowser.Controller.Net public WebSocketConnectingEventArgs() { - QueryString = new QueryParamCollection(); + QueryString = new QueryCollection(); AllowConnection = true; } } diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs index 579f80c968..50c6076f30 100644 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ b/MediaBrowser.Model/Services/IHttpRequest.cs @@ -2,11 +2,6 @@ namespace MediaBrowser.Model.Services { public interface IHttpRequest : IRequest { - /// - /// The HttpResponse - /// - IHttpResponse HttpResponse { get; } - /// /// The HTTP Verb /// diff --git a/MediaBrowser.Model/Services/IHttpResponse.cs b/MediaBrowser.Model/Services/IHttpResponse.cs index a8b79f3949..b99d12525d 100644 --- a/MediaBrowser.Model/Services/IHttpResponse.cs +++ b/MediaBrowser.Model/Services/IHttpResponse.cs @@ -4,17 +4,5 @@ namespace MediaBrowser.Model.Services { public interface IHttpResponse : IResponse { - //ICookies Cookies { get; } - - /// - /// Adds a new Set-Cookie instruction to Response - /// - /// - void SetCookie(Cookie cookie); - - /// - /// Removes all pending Set-Cookie instructions - /// - void ClearCookies(); } } diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 0fd4ea37b1..edb5a2509b 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -1,20 +1,15 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services { public interface IRequest { - /// - /// The underlying ASP.NET or HttpListener HttpRequest - /// - object OriginalRequest { get; } - IResponse Response { get; } /// @@ -51,9 +46,9 @@ namespace MediaBrowser.Model.Services /// Dictionary Items { get; } - QueryParamCollection Headers { get; } + IHeaderDictionary Headers { get; } - QueryParamCollection QueryString { get; } + IQueryCollection QueryString { get; } Task GetFormData(); @@ -122,21 +117,15 @@ namespace MediaBrowser.Model.Services Stream OutputStream { get; } - /// - /// Signal that this response has been handled and no more processing should be done. - /// When used in a request or response filter, no more filters or processing is done on this request. - /// - void Close(); - /// /// Gets a value indicating whether this instance is closed. /// - bool IsClosed { get; } + bool IsClosed { get; set; } //Add Metadata to Response Dictionary Items { get; } - QueryParamCollection Headers { get; } + IHeaderDictionary Headers { get; } Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken); diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 9f23b2420f..4631a3b633 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; -using System.Net; using MediaBrowser.Model.Dto; -using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services { @@ -13,23 +10,6 @@ namespace MediaBrowser.Model.Services { public QueryParamCollection() { - - } - - public QueryParamCollection(IHeaderDictionary headers) - { - foreach (var pair in headers) - { - Add(pair.Key, pair.Value); - } - } - - public QueryParamCollection(IQueryCollection queryCollection) - { - foreach (var pair in queryCollection) - { - Add(pair.Key, pair.Value); - } } private static StringComparison GetStringComparison() From 1e97e4d462b13baace363be5304bec70940a81a1 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 14:29:44 +0100 Subject: [PATCH 0098/6208] Remove IHttpResponse --- MediaBrowser.Model/Services/IHttpResponse.cs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 MediaBrowser.Model/Services/IHttpResponse.cs diff --git a/MediaBrowser.Model/Services/IHttpResponse.cs b/MediaBrowser.Model/Services/IHttpResponse.cs deleted file mode 100644 index b99d12525d..0000000000 --- a/MediaBrowser.Model/Services/IHttpResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Net; - -namespace MediaBrowser.Model.Services -{ - public interface IHttpResponse : IResponse - { - } -} From 7668ecf9c95fe7deb8211704c24266d5b17f6c8c Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Wed, 27 Feb 2019 18:20:48 +0000 Subject: [PATCH 0099/6208] Use Version Class to ease comparisons --- .../Encoder/EncoderValidator.cs | 105 ++++-------------- 1 file changed, 23 insertions(+), 82 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index be33ea58ef..5ebd189ace 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -8,66 +8,6 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { - public class FFmpegVersion - { - private readonly int _major; - private readonly int _minor; - - private const int _unknown = 0; - - public FFmpegVersion(string p1) - { - var match = Regex.Match(p1, @"(?\d+)\.(?\d+)"); - - if (match.Groups["major"].Success && match.Groups["minor"].Success) - { - _major = int.Parse(match.Groups["major"].Value); - _minor = int.Parse(match.Groups["minor"].Value); - } - else - { - _major = _unknown; - _minor = _unknown; - } - } - - public override string ToString() - { - switch (_major) - { - case _unknown: - return "Unknown"; - default: - return $"{_major}.{_minor}"; - } - } - - public bool Unknown() - { - return _major == _unknown; - } - - public bool Below(FFmpegVersion checkAgainst) - { - return ToScalar() < checkAgainst.ToScalar(); - } - - public bool Above(FFmpegVersion checkAgainst) - { - return ToScalar() > checkAgainst.ToScalar(); - } - - public bool Same(FFmpegVersion checkAgainst) - { - return ToScalar() == checkAgainst.ToScalar(); - } - - private int ToScalar() - { - return (_major * 1000) + _minor; - } - } - public class EncoderValidator { private readonly ILogger _logger; @@ -119,19 +59,19 @@ namespace MediaBrowser.MediaEncoding.Encoder } // The min and max FFmpeg versions required to run jellyfin successfully - FFmpegVersion minRequired = new FFmpegVersion("4.0"); - FFmpegVersion maxRequired = new FFmpegVersion("4.0"); + var minRequired = new Version(4, 0); + var maxRequired = new Version(4, 0); // Work out what the version under test is - FFmpegVersion underTest = GetFFmpegVersion(output); + var underTest = GetFFmpegVersion(output); if (logOutput) { - _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest.ToString()); + _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown"); - if (underTest.Unknown()) + if (underTest == null) // Version is unknown { - if (minRequired.Same(maxRequired)) + if (minRequired.Equals(maxRequired)) { _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString()); } @@ -140,21 +80,22 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString()); } } - else if (underTest.Below(minRequired)) + else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend { _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString()); } - else if (underTest.Above(maxRequired)) + else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend { _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString()); } - else + else // Version is ok { _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version"); } } - return !underTest.Below(minRequired) && !underTest.Above(maxRequired); + // underTest shall be null if versions is unknown + return (underTest == null) ? false : !(underTest.CompareTo(minRequired) < 0) && !(underTest.CompareTo(maxRequired) > 0); } /// @@ -166,36 +107,36 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// /// - static private FFmpegVersion GetFFmpegVersion(string output) + static private Version GetFFmpegVersion(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)"); if (match.Success) { - return new FFmpegVersion(match.Groups[1].Value); + return new Version(match.Groups[1].Value); } else { // Try and use the individual library versions to determine a FFmpeg version // This lookup table is to be maintained with the following command line: // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' - var lut = new ReadOnlyDictionary - (new Dictionary + var lut = new ReadOnlyDictionary + (new Dictionary { - { new FFmpegVersion("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, - { new FFmpegVersion("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," }, - { new FFmpegVersion("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," }, - { new FFmpegVersion("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," }, - { new FFmpegVersion("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," }, - { new FFmpegVersion("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," } + { new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, + { new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," }, + { new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," }, + { new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," }, + { new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," }, + { new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," } }); // Create a reduced version string and lookup key from dictionary var reducedVersion = GetVersionString(output); - var found = lut.FirstOrDefault(x => x.Value == reducedVersion).Key; - return found ?? new FFmpegVersion("Unknown"); + // Try to lookup the string and return Key, otherwise if not found returns null + return lut.FirstOrDefault(x => x.Value == reducedVersion).Key; } } From 647adc51c8681113afbc2c45b7637cc2c50f11f0 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 19:55:25 +0100 Subject: [PATCH 0100/6208] Fix query log --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 70753e5634..fd46c00578 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -321,7 +321,7 @@ namespace Emby.Server.Implementations.HttpServer string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; return newQueryString.Count > 0 - ? string.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) + ? $"{pagePathWithoutQueryString}?{newQueryString.Select(value => value.Key + " = " + string.Join(", ", value.Value))}" : pagePathWithoutQueryString; } From c0b95dbc791acae1c28bf50fb3a33c06935e7192 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 20:11:40 +0100 Subject: [PATCH 0101/6208] Fix query log for real --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index fd46c00578..dbfb5e2438 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -321,7 +321,7 @@ namespace Emby.Server.Implementations.HttpServer string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; return newQueryString.Count > 0 - ? $"{pagePathWithoutQueryString}?{newQueryString.Select(value => value.Key + " = " + string.Join(", ", value.Value))}" + ? $"{pagePathWithoutQueryString}?{string.Join("&", newQueryString.Select(value => value.Key + " = " + string.Join(", ", value.Value)))}" : pagePathWithoutQueryString; } From e47d12198556abfbfb57d2c1a9e45f6f7230bf63 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 21:40:47 +0100 Subject: [PATCH 0102/6208] Fix websockets --- .../SocketSharp/SharpWebSocket.cs | 5 +++++ .../SocketSharp/WebSocketSharpListener.cs | 13 +++++++++---- .../Net/BasePeriodicWebSocketListener.cs | 13 ++----------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index dcbfb80487..eebbe8b780 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -85,6 +85,11 @@ namespace Emby.Server.Implementations.SocketSharp if (dispose) { _cancellationTokenSource.Cancel(); + if (_webSocket.State == WebSocketState.Open) + { + _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", + CancellationToken.None); + } } _disposed = true; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 9f046c3fd0..c5abad5d33 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -76,18 +76,23 @@ using Microsoft.Extensions.Logging; Endpoint = endpoint }); - var buffer = WebSocket.CreateClientBuffer(4096, 4096); WebSocketReceiveResult result; var message = new List(); do { + var buffer = WebSocket.CreateServerBuffer(4096); result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); - socket.OnReceiveBytes(buffer.Array); message.AddRange(buffer.Array.Take(result.Count)); - } while (!result.EndOfMessage && result.MessageType != WebSocketMessageType.Close); - socket.OnReceiveBytes(message.ToArray()); + if (result.EndOfMessage) + { + socket.OnReceiveBytes(message.ToArray()); + message.Clear(); + } + } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); + + await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, result.CloseStatusDescription, _disposeCancellationToken); socket.Dispose(); diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 4242a00e21..d2d0fbbc6c 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -80,12 +80,7 @@ namespace MediaBrowser.Controller.Net protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected virtual bool SendOnTimer => false; - - protected virtual void ParseMessageParams(string[] values) - { - - } + protected bool SendOnTimer => false; /// /// Starts sending messages over a web socket @@ -98,11 +93,6 @@ namespace MediaBrowser.Controller.Net var dueTimeMs = long.Parse(vals[0], UsCulture); var periodMs = long.Parse(vals[1], UsCulture); - if (vals.Length > 2) - { - ParseMessageParams(vals.Skip(2).ToArray()); - } - var cancellationTokenSource = new CancellationTokenSource(); Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); @@ -250,6 +240,7 @@ namespace MediaBrowser.Controller.Net { Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + connection.Item1.Dispose(); var timer = connection.Item3; if (timer != null) From dab8e150520f5a76d875a926530632783e17006f Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 21:48:28 +0100 Subject: [PATCH 0103/6208] Check websocket state before closing --- .../SocketSharp/WebSocketSharpListener.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index c5abad5d33..9422673f52 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -93,8 +93,12 @@ using Microsoft.Extensions.Logging; } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); - await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, _disposeCancellationToken); + if (webSocketContext.State == WebSocketState.Open) + { + await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, _disposeCancellationToken); + } + socket.Dispose(); } else From 71ed8409446774c395b339728e2436c3b30536d8 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 22:09:22 +0100 Subject: [PATCH 0104/6208] Simplify websocket listeners --- .../ScheduledTasksWebSocketListener.cs | 3 +- .../Session/SessionInfoWebSocketListener.cs | 3 +- .../System/ActivityLogWebSocketListener.cs | 3 +- .../Net/BasePeriodicWebSocketListener.cs | 74 +++---------------- 4 files changed, 13 insertions(+), 70 deletions(-) diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs index b0c4b29c17..d24a187430 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs @@ -58,9 +58,8 @@ namespace MediaBrowser.Api.ScheduledTasks /// /// Gets the data to send. /// - /// The state. /// Task{IEnumerable{TaskInfo}}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken cancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(TaskManager.ScheduledTasks .OrderBy(i => i.Name) diff --git a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs index beb2fb11d0..b79e9f84b1 100644 --- a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs +++ b/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs @@ -79,9 +79,8 @@ namespace MediaBrowser.Api.Session /// /// Gets the data to send. /// - /// The state. /// Task{SystemInfo}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken cancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(_sessionManager.Sessions); } diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs index 43f3c5a223..a036619b81 100644 --- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs +++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs @@ -38,9 +38,8 @@ namespace MediaBrowser.Api.System /// /// Gets the data to send. /// - /// The state. /// Task{SystemInfo}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken CancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(new List()); } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index d2d0fbbc6c..5a6a0ea31e 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Net /// /// The _active connections /// - protected readonly List> ActiveConnections = - new List>(); + protected readonly List> ActiveConnections = + new List>(); /// /// Gets the name. @@ -34,9 +34,8 @@ namespace MediaBrowser.Controller.Net /// /// Gets the data to send. /// - /// The state. /// Task{`1}. - protected abstract Task GetDataToSend(TStateType state, CancellationToken cancellationToken); + protected abstract Task GetDataToSend(); /// /// The logger @@ -80,8 +79,6 @@ namespace MediaBrowser.Controller.Net protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected bool SendOnTimer => false; - /// /// Starts sending messages over a web socket /// @@ -97,10 +94,6 @@ namespace MediaBrowser.Controller.Net Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); - var timer = SendOnTimer ? - new Timer(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) : - null; - var state = new TStateType { IntervalMs = periodMs, @@ -109,47 +102,13 @@ namespace MediaBrowser.Controller.Net lock (ActiveConnections) { - ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, timer, state)); + ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, state)); } - - if (timer != null) - { - timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); - } - } - - /// - /// Timers the callback. - /// - /// The state. - private void TimerCallback(object state) - { - var connection = (IWebSocketConnection)state; - - Tuple tuple; - - lock (ActiveConnections) - { - tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); - } - - if (tuple == null) - { - return; - } - - if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) - { - DisposeConnection(tuple); - return; - } - - SendData(tuple); } protected void SendData(bool force) { - Tuple[] tuples; + Tuple[] tuples; lock (ActiveConnections) { @@ -158,7 +117,7 @@ namespace MediaBrowser.Controller.Net { if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) { - var state = c.Item4; + var state = c.Item3; if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs) { @@ -177,17 +136,17 @@ namespace MediaBrowser.Controller.Net } } - private async void SendData(Tuple tuple) + private async void SendData(Tuple tuple) { var connection = tuple.Item1; try { - var state = tuple.Item4; + var state = tuple.Item3; var cancellationToken = tuple.Item2.Token; - var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false); + var data = await GetDataToSend().ConfigureAwait(false); if (data != null) { @@ -236,24 +195,11 @@ namespace MediaBrowser.Controller.Net /// Disposes the connection. /// /// The connection. - private void DisposeConnection(Tuple connection) + private void DisposeConnection(Tuple connection) { Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); connection.Item1.Dispose(); - var timer = connection.Item3; - - if (timer != null) - { - try - { - timer.Dispose(); - } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - } - } try { From fb1de5a9213f7da98ed15a6975201d6bca3537d4 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 23:22:55 +0100 Subject: [PATCH 0105/6208] Remove more cruft and add the beginnings of a socket middleware --- .../ApplicationHost.cs | 2 + .../HttpServer/WebSocketConnection.cs | 36 +--------- .../Middleware/WebSocketMiddleware.cs | 36 ++++++++++ Emby.Server.Implementations/Net/IWebSocket.cs | 5 -- .../SocketSharp/WebSocketSharpListener.cs | 65 +++++++------------ .../WebSocket/WebSocketManager.cs | 7 ++ .../WebSockets/WebSocketManager.cs | 22 +++++++ .../MediaBrowser.Controller.csproj | 8 ++- 8 files changed, 97 insertions(+), 84 deletions(-) create mode 100644 Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs create mode 100644 Emby.Server.Implementations/WebSocket/WebSocketManager.cs create mode 100644 Emby.Server.Implementations/WebSockets/WebSocketManager.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0b4a2fd30e..e558b43544 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -35,6 +35,7 @@ using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; +using Emby.Server.Implementations.Middleware; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Reflection; @@ -641,6 +642,7 @@ namespace Emby.Server.Implementations app.UseWebSockets(); app.UseResponseCompression(); + // TODO app.UseMiddleware(); app.Use(ExecuteWebsocketHandlerAsync); app.Use(ExecuteHttpHandlerAsync); }) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 2bf460bd11..54a16040f7 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -102,12 +102,6 @@ namespace Emby.Server.Implementations.HttpServer _socket = socket; _socket.OnReceiveBytes = OnReceiveInternal; - var memorySocket = socket as IMemoryWebSocket; - if (memorySocket != null) - { - memorySocket.OnReceiveMemoryBytes = OnReceiveInternal; - } - RemoteEndPoint = remoteEndPoint; _logger = logger; @@ -143,34 +137,6 @@ namespace Emby.Server.Implementations.HttpServer } } - /// - /// Called when [receive]. - /// - /// The memory block. - /// The length of the memory block. - private void OnReceiveInternal(Memory memory, int length) - { - LastActivityDate = DateTime.UtcNow; - - if (OnReceive == null) - { - return; - } - - var bytes = memory.Slice(0, length).ToArray(); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; - - if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)) - { - OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length)); - } - else - { - OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length)); - } - } - private void OnReceiveInternal(string message) { LastActivityDate = DateTime.UtcNow; @@ -194,7 +160,7 @@ namespace Emby.Server.Implementations.HttpServer var info = new WebSocketMessageInfo { MessageType = stub.MessageType, - Data = stub.Data == null ? null : stub.Data.ToString(), + Data = stub.Data?.ToString(), Connection = this }; diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs new file mode 100644 index 0000000000..a1d0e77d65 --- /dev/null +++ b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager; + +namespace Emby.Server.Implementations.Middleware +{ + public class WebSocketMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly WebSocketManager _webSocketManager; + + public WebSocketMiddleware(RequestDelegate next, ILogger logger, WebSocketManager webSocketManager) + { + _next = next; + _logger = logger; + _webSocketManager = webSocketManager; + } + + public async Task Invoke(HttpContext httpContext) + { + _logger.LogInformation("Handling request: " + httpContext.Request.Path); + + if (httpContext.WebSockets.IsWebSocketRequest) + { + var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); + _webSocketManager.AddSocket(webSocketContext); + } + else + { + await _next.Invoke(httpContext); + } + } + } +} diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs index 4671de07c5..4d160aa66f 100644 --- a/Emby.Server.Implementations/Net/IWebSocket.cs +++ b/Emby.Server.Implementations/Net/IWebSocket.cs @@ -45,9 +45,4 @@ namespace Emby.Server.Implementations.Net /// Task. Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); } - - public interface IMemoryWebSocket - { - Action, int> OnReceiveMemoryBytes { get; set; } - } } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 9422673f52..5ddd316477 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -33,8 +33,6 @@ using Microsoft.Extensions.Logging; public Func ErrorHandler { get; set; } public Func RequestHandler { get; set; } - public Action WebSocketConnecting { get; set; } - public Action WebSocketConnected { get; set; } private static void LogRequest(ILogger logger, HttpRequest request) @@ -52,60 +50,41 @@ using Microsoft.Extensions.Logging; var endpoint = ctx.Connection.RemoteIpAddress.ToString(); var url = ctx.Request.GetDisplayUrl(); - var connectingArgs = new WebSocketConnectingEventArgs + var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); + var socket = new SharpWebSocket(webSocketContext, _logger); + + WebSocketConnected(new WebSocketConnectEventArgs { Url = url, QueryString = ctx.Request.Query, + WebSocket = socket, Endpoint = endpoint - }; + }); - WebSocketConnecting?.Invoke(connectingArgs); + WebSocketReceiveResult result; + var message = new List(); - if (connectingArgs.AllowConnection) + do { - _logger.LogDebug("Web socket connection allowed"); + var buffer = WebSocket.CreateServerBuffer(4096); + result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); + message.AddRange(buffer.Array.Take(result.Count)); - var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - var socket = new SharpWebSocket(webSocketContext, _logger); - - WebSocketConnected(new WebSocketConnectEventArgs + if (result.EndOfMessage) { - Url = url, - QueryString = ctx.Request.Query, - WebSocket = socket, - Endpoint = endpoint - }); - - WebSocketReceiveResult result; - var message = new List(); - - do - { - var buffer = WebSocket.CreateServerBuffer(4096); - result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); - message.AddRange(buffer.Array.Take(result.Count)); - - if (result.EndOfMessage) - { - socket.OnReceiveBytes(message.ToArray()); - message.Clear(); - } - } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); - - - if (webSocketContext.State == WebSocketState.Open) - { - await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, _disposeCancellationToken); + socket.OnReceiveBytes(message.ToArray()); + message.Clear(); } + } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); - socket.Dispose(); - } - else + + if (webSocketContext.State == WebSocketState.Open) { - _logger.LogWarning("Web socket connection not allowed"); - ctx.Response.StatusCode = 401; + await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, _disposeCancellationToken); } + + socket.Dispose(); } catch (Exception ex) { diff --git a/Emby.Server.Implementations/WebSocket/WebSocketManager.cs b/Emby.Server.Implementations/WebSocket/WebSocketManager.cs new file mode 100644 index 0000000000..7472820cf2 --- /dev/null +++ b/Emby.Server.Implementations/WebSocket/WebSocketManager.cs @@ -0,0 +1,7 @@ +namespace Emby.Server.Implementations.WebSocket +{ + public class WebSocketManager + { + + } +} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs new file mode 100644 index 0000000000..7e74a45277 --- /dev/null +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Concurrent; +using System.Net.WebSockets; + +namespace Emby.Server.Implementations.WebSockets +{ + public class WebSocketManager + { + private readonly ConcurrentDictionary _activeWebSockets; + + public WebSocketManager() + { + _activeWebSockets = new ConcurrentDictionary(); + } + + public void AddSocket(WebSocket webSocket) + { + var guid = Guid.NewGuid(); + _activeWebSockets.TryAdd(guid, webSocket); + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 01893f1b55..81e255d525 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors @@ -16,6 +16,12 @@ + + + ..\..\..\..\..\usr\local\share\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.extensions\2.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.Extensions.dll + + + netstandard2.0 false From 588a13377c347cbd56462c64b80f0b9a563e534f Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 23:24:43 +0100 Subject: [PATCH 0106/6208] Remove a file that shouldn't have been added --- Emby.Server.Implementations/WebSocket/WebSocketManager.cs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 Emby.Server.Implementations/WebSocket/WebSocketManager.cs diff --git a/Emby.Server.Implementations/WebSocket/WebSocketManager.cs b/Emby.Server.Implementations/WebSocket/WebSocketManager.cs deleted file mode 100644 index 7472820cf2..0000000000 --- a/Emby.Server.Implementations/WebSocket/WebSocketManager.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Emby.Server.Implementations.WebSocket -{ - public class WebSocketManager - { - - } -} From 1ac282b12e20abc91402015369dc17543604f9fb Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Feb 2019 23:39:20 +0100 Subject: [PATCH 0107/6208] Call SharpWebSocket's Closed event handler before disposing --- Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index eebbe8b780..62b16ed8c8 100644 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -90,6 +90,7 @@ namespace Emby.Server.Implementations.SocketSharp _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", CancellationToken.None); } + Closed?.Invoke(this, EventArgs.Empty); } _disposed = true; From 1d1e6dede94596dc4e613a59c1561fe96447581c Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Wed, 27 Feb 2019 20:56:50 -0500 Subject: [PATCH 0108/6208] Set ffmpeg+ffprobe paths in Docker container Will always ensure containers use correct path. Yes - the arm images have a different path than the amd64 one. This is caused by the amd64 image using ffmpeg from jellyfin/ffmpeg while the others use ffmpeg from their distro's repos. --- Dockerfile | 6 +++++- Dockerfile.arm | 6 +++++- Dockerfile.arm64 | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3562688419..e2d424df78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,8 @@ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/local/bin/ffmpeg \ + --ffprobe /usr/local/bin/ffprobe diff --git a/Dockerfile.arm b/Dockerfile.arm index 69c5d446e8..d220763a2c 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -30,4 +30,8 @@ RUN apt-get update \ COPY --from=builder /jellyfin /jellyfin EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/bin/ffmpeg \ + --ffprobe /usr/bin/ffprobe diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index b85f8735bd..243b841e92 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -31,4 +31,8 @@ RUN apt-get update \ COPY --from=builder /jellyfin /jellyfin EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache +ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ + --datadir /config \ + --cachedir /cache \ + --ffmpeg /usr/bin/ffmpeg \ + --ffprobe /usr/bin/ffprobe From edba82db373da8fbab8159d6ab2483052ebab231 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 27 Feb 2019 23:05:12 -0800 Subject: [PATCH 0109/6208] fixed logic flip in auth empty check and fixed crypto algo choice --- .../Cryptography/CryptographyProvider.cs | 34 +++++++++++++------ .../Library/DefaultAuthenticationProvider.cs | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index ea719309cd..3c9403ba8e 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; +using System.Linq; using MediaBrowser.Model.Cryptography; namespace Emby.Server.Implementations.Cryptography @@ -11,16 +12,18 @@ namespace Emby.Server.Implementations.Cryptography public class CryptographyProvider : ICryptoProvider { private HashSet SupportedHashMethods; - public string DefaultHashMethod => "SHA256"; + public string DefaultHashMethod => "PBKDF2"; private RandomNumberGenerator rng; private int defaultiterations = 1000; public CryptographyProvider() { + //FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one + //Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 SupportedHashMethods = new HashSet() { - "MD5" + "MD5" ,"System.Security.Cryptography.MD5" ,"SHA" ,"SHA1" @@ -75,10 +78,15 @@ namespace Emby.Server.Implementations.Cryptography private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) { //downgrading for now as we need this library to be dotnetstandard compliant - using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) + //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment + if(method == DefaultHashMethod) { - return r.GetBytes(32); + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) + { + return r.GetBytes(32); + } } + throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); } public byte[] ComputeHash(string HashMethod, byte[] bytes) @@ -93,18 +101,22 @@ namespace Emby.Server.Implementations.Cryptography public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) { - if (SupportedHashMethods.Contains(HashMethod)) + if(HashMethod == DefaultHashMethod) { - if (salt.Length == 0) + return PBKDF2(HashMethod, bytes, salt, defaultiterations); + } + else if (SupportedHashMethods.Contains(HashMethod)) + { + using (var h = HashAlgorithm.Create(HashMethod)) { - using (var h = HashAlgorithm.Create(HashMethod)) + if (salt.Length == 0) { return h.ComputeHash(bytes); } - } - else - { - return PBKDF2(HashMethod, bytes, salt, defaultiterations); + else + { + return h.ComputeHash(bytes.Concat(salt).ToArray()); + } } } else diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index b58374adb4..7ccdccc0ad 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Library //but at least they are in the new format. private void ConvertPasswordFormat(User user) { - if (!string.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(user.Password)) { return; } From 4951ec98140e38f561cf3a09c9bc078cf1a88852 Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Thu, 28 Feb 2019 18:00:25 +0800 Subject: [PATCH 0110/6208] Fix rmvb video can not play under DLNA Or will report "Could not find handler for /videos/xxx/stream.rm" error in server side. Test OK with Kodi and gupnp-tools. --- MediaBrowser.Api/Playback/Progressive/VideoService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 7aeb0e9e85..bf15cc756c 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive [Route("/Videos/{Id}/stream.mov", "GET")] [Route("/Videos/{Id}/stream.iso", "GET")] [Route("/Videos/{Id}/stream.flv", "GET")] + [Route("/Videos/{Id}/stream.rm", "GET")] [Route("/Videos/{Id}/stream", "GET")] [Route("/Videos/{Id}/stream.ts", "HEAD")] [Route("/Videos/{Id}/stream.webm", "HEAD")] From c84729a4f462205275fed77f896d673b6f718342 Mon Sep 17 00:00:00 2001 From: The Lynxy Date: Thu, 28 Feb 2019 07:50:32 -0500 Subject: [PATCH 0111/6208] Do not allow new users to delete content by default --- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 23805b79f5..27ce23778e 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users public UserPolicy() { - EnableContentDeletion = true; + EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty(); EnableSyncTranscoding = true; From 95d001a053351752f52802ebc7d855333019f8cc Mon Sep 17 00:00:00 2001 From: Xu Fasheng Date: Thu, 28 Feb 2019 22:15:59 +0800 Subject: [PATCH 0112/6208] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0831e1340e..4b397b3280 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -22,6 +22,7 @@ - [Liggy](https://github.com/Liggy) - [fruhnow](https://github.com/fruhnow) - [Lynxy](https://github.com/Lynxy) + - [fasheng](https://github.com/fasheng) # Emby Contributors From f1086a72bf1ebf486464c877efb66bb4f87771fe Mon Sep 17 00:00:00 2001 From: Vasily Date: Thu, 28 Feb 2019 15:58:41 +0000 Subject: [PATCH 0113/6208] Improve logic when determining return value Co-Authored-By: ploughpuff <33969763+ploughpuff@users.noreply.github.com> --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 5ebd189ace..f725d2c019 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } // underTest shall be null if versions is unknown - return (underTest == null) ? false : !(underTest.CompareTo(minRequired) < 0) && !(underTest.CompareTo(maxRequired) > 0); + return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0); } /// From 58e5931a3287f2f50a58bcc1e2991375fc06c93f Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 28 Feb 2019 15:34:08 -0500 Subject: [PATCH 0114/6208] Bump version to 10.2.2 --- SharedVersion.cs | 4 ++-- .../debian-package-x64/pkg-src/changelog | 17 +++++++++++++++++ .../fedora-package-x64/pkg-src/jellyfin.spec | 19 ++++++++++++++----- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/SharedVersion.cs b/SharedVersion.cs index 41eda393a5..785ba93018 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.2.1")] -[assembly: AssemblyFileVersion("10.2.1")] +[assembly: AssemblyVersion("10.2.2")] +[assembly: AssemblyFileVersion("10.2.2")] diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 7b7efff278..349e8787f6 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,20 @@ +jellyfin (10.2.2-1) unstable; urgency=medium + + * jellyfin: + * PR968 Release 10.2.z copr autobuild + * PR964 Install the dotnet runtime package in Fedora build + * PR979 Build Package releases without debug turned on + * PR990 Fix slow local image validation + * PR991 Fix the ffmpeg compatibility + * PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild + * PR998 Set EnableRaisingEvents to true for processes that require it + * PR1017 Set ffmpeg+ffprobe paths in Docker container + * jellyfin-web: + * PR152 Go back on Media stop + * PR156 Fix volume slider not working on nowplayingbar + + -- Jellyfin Packaging Team Thu, 28 Feb 2019 15:32:16 -0500 + jellyfin (10.2.1-1) unstable; urgency=medium * jellyfin: diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 68e0529cff..e24bd2fcb1 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,8 +7,8 @@ %endif Name: jellyfin -Version: 10.2.1 -Release: 2%{?dist} +Version: 10.2.2 +Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 URL: https://jellyfin.media @@ -140,10 +140,19 @@ fi %systemd_postun_with_restart jellyfin.service %changelog -* Thu Feb 21 2019 Brian J. Murrell +* Thu Feb 28 2019 Jellyfin Packaging Team - jellyfin: -- dotnet seems to have moved to dotnet-runtime -- COPR auto-build +- PR968 Release 10.2.z copr autobuild +- PR964 Install the dotnet runtime package in Fedora build +- PR979 Build Package releases without debug turned on +- PR990 Fix slow local image validation +- PR991 Fix the ffmpeg compatibility +- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild +- PR998 Set EnableRaisingEvents to true for processes that require it +- PR1017 Set ffmpeg+ffprobe paths in Docker container +- jellyfin-web: +- PR152 Go back on Media stop +- PR156 Fix volume slider not working on nowplayingbar * Wed Feb 20 2019 Jellyfin Packaging Team - jellyfin: - PR920 Fix cachedir missing from Docker container From 331260cf803f7d4500d2b88eabb78be994425081 Mon Sep 17 00:00:00 2001 From: TheBird956 Date: Tue, 26 Feb 2019 03:38:39 +0000 Subject: [PATCH 0115/6208] Translated using Weblate (French) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- Emby.Server.Implementations/Localization/Core/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 52afb4e492..e434b7605b 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -44,7 +44,7 @@ "NameInstallFailed": "{0} échec d'installation", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", - "NewVersionIsAvailable": "Une nouvelle version d'Jellyfin Serveur est disponible au téléchargement.", + "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.", "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", "NotificationOptionAudioPlayback": "Lecture audio démarrée", @@ -89,7 +89,7 @@ "UserOnlineFromDevice": "{0} s'est connecté depuis {1}", "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", - "UserStartedPlayingItemWithValues": "{0} est entrain de lire {1} sur {2}", + "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie", "ValueSpecialEpisodeName": "Spécial - {0}", From c8788d83fbd921fccf80b7f49be83db2c73a210b Mon Sep 17 00:00:00 2001 From: TheBird956 Date: Tue, 26 Feb 2019 03:23:03 +0000 Subject: [PATCH 0116/6208] Translated using Weblate (French (Canada)) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/ --- .../Localization/Core/fr-CA.json | 170 +++++++++--------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 7202be9f56..4b4db39a8b 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -1,97 +1,97 @@ { "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", + "AppDeviceValues": "Application : {0}, Appareil : {1}", "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", + "Artists": "Artistes", + "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", + "Books": "Livres", + "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}", + "Channels": "Chaînes", + "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", + "DeviceOfflineWithName": "{0} s'est déconnecté", + "DeviceOnlineWithName": "{0} est connecté", + "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}", + "Favorites": "Favoris", + "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Artistes de l'album", + "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", + "HeaderFavoriteAlbums": "Albums favoris", + "HeaderFavoriteArtists": "Artistes favoris", + "HeaderFavoriteEpisodes": "Épisodes favoris", + "HeaderFavoriteShows": "Séries favorites", + "HeaderFavoriteSongs": "Chansons favorites", + "HeaderLiveTV": "TV en direct", "HeaderNextUp": "À Suivre", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "HeaderRecordingGroups": "Groupes d'enregistrements", + "HomeVideos": "Vidéos personnelles", + "Inherit": "Hériter", + "ItemAddedWithName": "{0} a été ajouté à la médiathèque", + "ItemRemovedWithName": "{0} a été supprimé de la médiathèque", + "LabelIpAddressValue": "Adresse IP : {0}", + "LabelRunningTimeValue": "Durée : {0}", + "Latest": "Derniers", + "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", + "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour", + "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour", + "MixedContent": "Contenu mixte", + "Movies": "Films", + "Music": "Musique", + "MusicVideos": "Vidéos musicales", + "NameInstallFailed": "{0} échec d'installation", + "NameSeasonNumber": "Saison {0}", + "NameSeasonUnknown": "Saison Inconnue", + "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.", + "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", + "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", + "NotificationOptionAudioPlayback": "Lecture audio démarrée", + "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée", + "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée", + "NotificationOptionInstallationFailed": "Échec d'installation", + "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", + "NotificationOptionPluginError": "Erreur d'extension", + "NotificationOptionPluginInstalled": "Extension installée", + "NotificationOptionPluginUninstalled": "Extension désinstallée", + "NotificationOptionPluginUpdateInstalled": "Mise à jour d'extension installée", + "NotificationOptionServerRestartRequired": "Un redémarrage du serveur est requis", + "NotificationOptionTaskFailed": "Échec de tâche planifiée", + "NotificationOptionUserLockedOut": "Utilisateur verrouillé", + "NotificationOptionVideoPlayback": "Lecture vidéo démarrée", + "NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée", "Photos": "Photos", - "Playlists": "Playlists", - "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Series", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Playlists": "Listes de lecture", + "Plugin": "Extension", + "PluginInstalledWithName": "{0} a été installé", + "PluginUninstalledWithName": "{0} a été désinstallé", + "PluginUpdatedWithName": "{0} a été mis à jour", + "ProviderValue": "Fournisseur : {0}", + "ScheduledTaskFailedWithName": "{0} a échoué", + "ScheduledTaskStartedWithName": "{0} a commencé", + "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", + "Shows": "Émissions", + "Songs": "Chansons", + "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", - "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", + "SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés", + "Sync": "Synchroniser", + "System": "Système", + "TvShows": "Séries Télé", + "User": "Utilisateur", + "UserCreatedWithName": "L'utilisateur {0} a été créé", + "UserDeletedWithName": "L'utilisateur {0} a été supprimé", + "UserDownloadingItemWithValues": "{0} est en train de télécharger {1}", + "UserLockedOutWithName": "L'utilisateur {0} a été verrouillé", + "UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}", + "UserOnlineFromDevice": "{0} s'est connecté depuis {1}", + "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", + "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", + "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", + "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", + "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}" } From 7c13021def4432123917b6cf18c129e19ddff756 Mon Sep 17 00:00:00 2001 From: EffeF Date: Sun, 24 Feb 2019 17:44:06 +0000 Subject: [PATCH 0117/6208] Translated using Weblate (Italian) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- .../Localization/Core/it.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index a5f1e8f94d..357883cd37 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -34,17 +34,17 @@ "LabelRunningTimeValue": "Durata: {0}", "Latest": "Più recenti", "MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MixedContent": "Contenuto misto", "Movies": "Film", "Music": "Musica", "MusicVideos": "Video musicali", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "{0} installazione fallita", "NameSeasonNumber": "Stagione {0}", "NameSeasonUnknown": "Stagione sconosciuto", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.", "NotificationOptionApplicationUpdateAvailable": "Aggiornamento dell'applicazione disponibile", "NotificationOptionApplicationUpdateInstalled": "Aggiornamento dell'applicazione installato", "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", @@ -70,12 +70,12 @@ "ProviderValue": "Provider: {0}", "ScheduledTaskFailedWithName": "{0} fallito", "ScheduledTaskStartedWithName": "{0} avviati", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "Shows": "Programmi", "Songs": "Canzoni", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "SubtitlesDownloadedForItem": "Sottotitoli scaricati per {0}", "Sync": "Sincronizza", "System": "Sistema", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "La politica dell'utente è stata aggiornata per {0}", "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1}", "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale", "ValueSpecialEpisodeName": "Speciale - {0}", "VersionNumber": "Versione {0}" } From 311caf3fc3e45cc332e922897f963b95cb60f98a Mon Sep 17 00:00:00 2001 From: v1tin Date: Mon, 18 Feb 2019 19:02:01 +0000 Subject: [PATCH 0118/6208] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- .../Localization/Core/pt-BR.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index aaedf08505..dbc9c4c4b8 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -5,7 +5,7 @@ "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Books": "Livros", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Uma nova imagem da câmera foi submetida de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", "Collections": "Coletâneas", @@ -30,21 +30,21 @@ "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", - "LabelIpAddressValue": "Endereço ip: {0}", + "LabelIpAddressValue": "Endereço IP: {0}", "LabelRunningTimeValue": "Tempo de execução: {0}", "Latest": "Recente", "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "O Servidor Jellyfin foi atualizado para {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A seção {0} da configuração do servidor foi atualizada", "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada", "MixedContent": "Conteúdo misto", "Movies": "Filmes", "Music": "Música", "MusicVideos": "Vídeos musicais", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "A instalação de {0} falhou", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconhecida", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.", "NotificationOptionApplicationUpdateAvailable": "Atualização de aplicativo disponível", "NotificationOptionApplicationUpdateInstalled": "Atualização de aplicativo instalada", "NotificationOptionAudioPlayback": "Reprodução de áudio iniciada", @@ -70,12 +70,12 @@ "ProviderValue": "Provedor: {0}", "ScheduledTaskFailedWithName": "{0} falhou", "ScheduledTaskStartedWithName": "{0} iniciada", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "O servidor {0} precisa ser reiniciado", "Shows": "Séries", "Songs": "Músicas", "StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor tente novamente em breve.", "SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}", "SubtitlesDownloadedForItem": "Legendas baixadas para {0}", "Sync": "Sincronizar", "System": "Sistema", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}", "UserStartedPlayingItemWithValues": "{0} iniciou a reprodução de {1}", "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} foi adicionado a sua biblioteca", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versão {0}" } From d0e4e0f6004644b58cd5f64a89f82467759e5876 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Tue, 26 Feb 2019 23:05:46 +0000 Subject: [PATCH 0119/6208] Translated using Weblate (Slovenian) Currently translated at 87.2% (82 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/ --- .../Localization/Core/sl-SI.json | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index e850257d4a..b50ff4706e 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -1,62 +1,62 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", + "Albums": "Albumi", + "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", + "Application": "Aplikacija", + "Artists": "Izvajalci", + "AuthenticationSucceededWithUserName": "{0} preverjanje uspešno", + "Books": "Knjige", + "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}", + "Channels": "Kanali", + "ChapterNameValue": "Poglavje {0}", + "Collections": "Zbirke", "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "DeviceOnlineWithName": "{0} je povezan", + "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", + "Favorites": "Priljubljeni", + "Folders": "Mape", + "Genres": "Zvrsti", + "HeaderAlbumArtists": "Izvajalci albuma", + "HeaderCameraUploads": "Posnetki kamere", + "HeaderContinueWatching": "Nadaljuj gledanje", + "HeaderFavoriteAlbums": "Priljubljeni albumi", + "HeaderFavoriteArtists": "Priljubljeni izvajalci", + "HeaderFavoriteEpisodes": "Priljubljene epizode", + "HeaderFavoriteShows": "Priljubljene serije", + "HeaderFavoriteSongs": "Priljubljene pesmi", + "HeaderLiveTV": "TV v živo", + "HeaderNextUp": "Sledi", + "HeaderRecordingGroups": "Zbirke posnetkov", + "HomeVideos": "Domači posnetki", + "Inherit": "Podeduj", + "ItemAddedWithName": "{0} je dodan v knjižnico", + "ItemRemovedWithName": "{0} je bil odstranjen iz knjižnice", + "LabelIpAddressValue": "IP naslov: {0}", + "LabelRunningTimeValue": "Čas trajanja: {0}", + "Latest": "Najnovejše", + "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", + "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", + "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", + "MixedContent": "Razne vsebine", + "Movies": "Filmi", + "Music": "Glasba", + "MusicVideos": "Glasbeni posnetki", + "NameInstallFailed": "{0} namestitev neuspešna", + "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Season neznana", + "NewVersionIsAvailable": "Nova razničica Jellyfin strežnika je na voljo za prenos.", + "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo", + "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena", + "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto", + "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno", + "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen", + "NotificationOptionInstallationFailed": "Napaka pri nameščanju", + "NotificationOptionNewLibraryContent": "Nove vsebine dodane", + "NotificationOptionPluginError": "Napaka dodatka", + "NotificationOptionPluginInstalled": "Dodatek nameščen", + "NotificationOptionPluginUninstalled": "Dodatek odstranjen", + "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", + "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", "NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", From 53ed6e5e6efb69572a61cb57a31b600f4ba5c02b Mon Sep 17 00:00:00 2001 From: ElFantasma Date: Fri, 22 Feb 2019 18:49:18 +0000 Subject: [PATCH 0120/6208] Translated using Weblate (Spanish (Argentina)) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- .../Localization/Core/es-AR.json | 184 +++++++++--------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index c01bb0c501..dc73ba6b34 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -1,97 +1,97 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "Albums": "Álbumes", + "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", + "Application": "Aplicación", + "Artists": "Artistas", + "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", + "Books": "Libros", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "Channels": "Canales", + "ChapterNameValue": "Capítulo {0}", + "Collections": "Colecciones", + "DeviceOfflineWithName": "{0} se ha desconectado", + "DeviceOnlineWithName": "{0} está conectado", + "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}", + "Favorites": "Favoritos", + "Folders": "Carpetas", + "Genres": "Géneros", + "HeaderAlbumArtists": "Artistas de álbumes", + "HeaderCameraUploads": "Subidas de cámara", + "HeaderContinueWatching": "Continuar viendo", + "HeaderFavoriteAlbums": "Álbumes favoritos", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteEpisodes": "Episodios favoritos", + "HeaderFavoriteShows": "Programas favoritos", + "HeaderFavoriteSongs": "Canciones favoritas", + "HeaderLiveTV": "TV en vivo", + "HeaderNextUp": "Continuar Viendo", + "HeaderRecordingGroups": "Grupos de grabación", + "HomeVideos": "Videos caseros", + "Inherit": "Heredar", + "ItemAddedWithName": "{0} se ha añadido a la biblioteca", + "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", + "LabelIpAddressValue": "Dirección IP: {0}", + "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", + "Latest": "Últimos", + "MessageApplicationUpdated": "El servidor Jellyfin fue actualizado", + "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Fue actualizada la sección {0} de la configuración del servidor", + "MessageServerConfigurationUpdated": "Fue actualizada la configuración del servidor", + "MixedContent": "Contenido mixto", + "Movies": "Películas", + "Music": "Música", + "MusicVideos": "Videos musicales", + "NameInstallFailed": "{0} error de instalación", + "NameSeasonNumber": "Temporada {0}", + "NameSeasonUnknown": "Temporada desconocida", + "NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.", + "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", + "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", + "NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio", + "NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada", + "NotificationOptionInstallationFailed": "Error de instalación", + "NotificationOptionNewLibraryContent": "Nuevo contenido añadido", + "NotificationOptionPluginError": "Error en plugin", + "NotificationOptionPluginInstalled": "Plugin instalado", + "NotificationOptionPluginUninstalled": "Plugin desinstalado", + "NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada", + "NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor", + "NotificationOptionTaskFailed": "Error de tarea programada", + "NotificationOptionUserLockedOut": "Usuario bloqueado", + "NotificationOptionVideoPlayback": "Se inició la reproducción de video", + "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida", + "Photos": "Fotos", + "Playlists": "Listas de reproducción", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "PluginInstalledWithName": "{0} fue instalado", + "PluginUninstalledWithName": "{0} fue desinstalado", + "PluginUpdatedWithName": "{0} fue actualizado", + "ProviderValue": "Proveedor: {0}", + "ScheduledTaskFailedWithName": "{0} falló", + "ScheduledTaskStartedWithName": "{0} iniciada", + "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "Shows": "Series", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Songs": "Canciones", + "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", - "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", + "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}", + "Sync": "Sincronizar", + "System": "Sistema", + "TvShows": "Series de TV", + "User": "Usuario", + "UserCreatedWithName": "El usuario {0} ha sido creado", + "UserDeletedWithName": "El usuario {0} ha sido borrado", + "UserDownloadingItemWithValues": "{0} está descargando {1}", + "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", + "UserOfflineFromDevice": "{0} se ha desconectado de {1}", + "UserOnlineFromDevice": "{0} está en línea desde {1}", + "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", + "UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}", + "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", + "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia", + "ValueSpecialEpisodeName": "Especial - {0}", + "VersionNumber": "Versión {0}" } From 3d3d879b99776e6d156773aec5693ee7c63479ea Mon Sep 17 00:00:00 2001 From: Deniz Date: Fri, 22 Feb 2019 08:15:26 +0000 Subject: [PATCH 0121/6208] Translated using Weblate (Turkish) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- .../Localization/Core/tr.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 495f82db6d..9e00eba62f 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -1,12 +1,12 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", + "Albums": "Albümler", + "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", + "Application": "Uygulama", + "Artists": "Sanatçılar", + "AuthenticationSucceededWithUserName": "{0} başarı ile giriş yaptı", + "Books": "Kitaplar", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", + "Channels": "Kanallar", "ChapterNameValue": "Chapter {0}", "Collections": "Collections", "DeviceOfflineWithName": "{0} has disconnected", @@ -17,8 +17,8 @@ "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", + "HeaderContinueWatching": "İzlemeye Devam Et", + "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteShows": "Favori Showlar", @@ -30,21 +30,21 @@ "Inherit": "Inherit", "ItemAddedWithName": "{0} was added to the library", "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", + "LabelIpAddressValue": "Ip adresi: {0}", + "LabelRunningTimeValue": "Çalışma süresi: {0}", "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", + "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MixedContent": "Mixed content", "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "Music": "Müzik", + "MusicVideos": "Müzik videoları", + "NameInstallFailed": "{0} kurulum başarısız", + "NameSeasonNumber": "Sezon {0}", + "NameSeasonUnknown": "Bilinmeyen Sezon", + "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionAudioPlayback": "Audio playback started", From 27f9981142b4a75ceaff3a4b0f13f40b7d9467d6 Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Fri, 1 Mar 2019 00:17:46 -0500 Subject: [PATCH 0122/6208] Treat jellyfin-web as just another dependency for Docker builds --- Dockerfile | 6 ++++++ Dockerfile.arm | 6 ++++++ Dockerfile.arm64 | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/Dockerfile b/Dockerfile index e2d424df78..91a4f5a2d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,12 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ diff --git a/Dockerfile.arm b/Dockerfile.arm index d220763a2c..42f0354a32 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -28,6 +28,12 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 243b841e92..d3103d3893 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -29,6 +29,12 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin + +ARG JELLYFIN_WEB_VERSION=10.2.2 +RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && rm -rf /jellyfin/jellyfin-web \ + && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web + EXPOSE 8096 VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ From 6bdb5debd2492b71d11f9628889b8c29b6321a77 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Fri, 1 Mar 2019 14:08:51 +0100 Subject: [PATCH 0123/6208] Add some websocket manager boilerplate --- .../Middleware/WebSocketMiddleware.cs | 5 +- .../WebSockets/WebSocketHandler.cs | 10 +++ .../WebSockets/WebSocketManager.cs | 90 +++++++++++++++++-- 3 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 Emby.Server.Implementations/WebSockets/WebSocketHandler.cs diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs index a1d0e77d65..268bf4042d 100644 --- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs +++ b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs @@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Middleware if (httpContext.WebSockets.IsWebSocketRequest) { var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - _webSocketManager.AddSocket(webSocketContext); + if (webSocketContext != null) + { + await _webSocketManager.OnWebSocketConnected(webSocketContext); + } } else { diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs new file mode 100644 index 0000000000..70b9e85aa9 --- /dev/null +++ b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace Emby.Server.Implementations.WebSockets +{ + public interface IWebSocketHandler + { + Task ProcessMessage(WebSocketMessage message); + } +} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs index 7e74a45277..888f2f0fce 100644 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -1,22 +1,100 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; +using UtfUnknown; namespace Emby.Server.Implementations.WebSockets { public class WebSocketManager { - private readonly ConcurrentDictionary _activeWebSockets; + private readonly IWebSocketHandler[] _webSocketHandlers; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private const int BufferSize = 4096; - public WebSocketManager() + public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger logger) { - _activeWebSockets = new ConcurrentDictionary(); + _webSocketHandlers = webSocketHandlers; + _jsonSerializer = jsonSerializer; + _logger = logger; } - public void AddSocket(WebSocket webSocket) + public async Task OnWebSocketConnected(WebSocket webSocket) { - var guid = Guid.NewGuid(); - _activeWebSockets.TryAdd(guid, webSocket); + var taskCompletionSource = new TaskCompletionSource(); + var cancellationToken = new CancellationTokenSource().Token; + WebSocketReceiveResult result; + var message = new List(); + + do + { + var buffer = WebSocket.CreateServerBuffer(BufferSize); + result = await webSocket.ReceiveAsync(buffer, cancellationToken); + message.AddRange(buffer.Array.Take(result.Count)); + + if (result.EndOfMessage) + { + await ProcessMessage(message.ToArray(), taskCompletionSource); + message.Clear(); + } + } while (!taskCompletionSource.Task.IsCompleted && + webSocket.State == WebSocketState.Open && + result.MessageType != WebSocketMessageType.Close); + + if (webSocket.State == WebSocketState.Open) + { + await webSocket.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, cancellationToken); + } + } + + public async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource taskCompletionSource) + { + var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName; + var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) + ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length) + : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length); + + // All messages are expected to be json + if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message); + return; + } + + try + { + var info = _jsonSerializer.DeserializeFromString>(message); + + _logger.LogDebug("Websocket message received: {0}", info.MessageType); + + var tasks = _webSocketHandlers.Select(handler => Task.Run(() => + { + try + { + handler.ProcessMessage(info).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "{0} failed processing WebSocket message {1}", handler.GetType().Name, info.MessageType ?? string.Empty); + } + })); + + await Task.WhenAll(tasks); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing web socket message"); + } } } } From 9993dafe54fe4310d1008434405198d822ef51cc Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 1 Mar 2019 17:12:22 +0100 Subject: [PATCH 0124/6208] Don't mix LINQ and roreach loops for readability --- DvdLib/Ifo/Dvd.cs | 16 +++++----- Emby.Dlna/ContentDirectory/ControlHandler.cs | 4 +-- Emby.Dlna/Didl/DidlBuilder.cs | 3 +- Emby.Dlna/DlnaManager.cs | 21 ++++++------ Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Emby.Dlna/PlayTo/TransportCommands.cs | 19 +++++++++++ Emby.Naming/TV/EpisodePathParser.cs | 32 ++++++++++++------- .../Activity/ActivityManager.cs | 7 +++- .../ApplicationHost.cs | 2 +- .../Channels/ChannelManager.cs | 11 ++----- .../Data/SqliteUserDataRepository.cs | 4 +-- 11 files changed, 74 insertions(+), 47 deletions(-) diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index f784be83e7..90125fa3e3 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -26,17 +26,17 @@ namespace DvdLib.Ifo if (vmgPath == null) { - var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)); - - foreach (var ifo in allIfos) + foreach (var ifo in allFiles) { - var num = ifo.Name.Split('_').ElementAtOrDefault(1); - var numbersRead = new List(); + if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber)) + var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries); + if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) { ReadVTS(ifoNumber, ifo.FullName); - numbersRead.Add(ifoNumber); } } } @@ -76,7 +76,7 @@ namespace DvdLib.Ifo } } - private void ReadVTS(ushort vtsNum, List allFiles) + private void ReadVTS(ushort vtsNum, IEnumerable allFiles) { var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 1150afdbab..84f38ff769 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); + var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); } @@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory } else { - var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount)); + var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); totalCount = childrenResult.TotalRecordCount; provided = childrenResult.Items.Length; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 605f4f37b2..1268f3d5c2 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl { AddCommonFields(item, itemStubType, context, writer, filter); - var hasArtists = item as IHasArtist; var hasAlbumArtists = item as IHasAlbumArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { foreach (var artist in hasArtists.Artists) { diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index f53d274516..d6ee5d13ac 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -29,7 +29,7 @@ namespace Emby.Dlna private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; - private readonly IAssemblyInfo _assemblyInfo; + private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal); @@ -39,8 +39,7 @@ namespace Emby.Dlna IApplicationPaths appPaths, ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, - IServerApplicationHost appHost, - IAssemblyInfo assemblyInfo) + IServerApplicationHost appHost) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; @@ -48,7 +47,6 @@ namespace Emby.Dlna _logger = loggerFactory.CreateLogger("Dlna"); _jsonSerializer = jsonSerializer; _appHost = appHost; - _assemblyInfo = assemblyInfo; } public async Task InitProfilesAsync() @@ -368,15 +366,18 @@ namespace Emby.Dlna var systemProfilesPath = SystemProfilesPath; - foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType()) - .Where(i => i.StartsWith(namespaceName)) - .ToList()) + foreach (var name in _assembly.GetManifestResourceNames()) { + if (!name.StartsWith(namespaceName)) + { + continue; + } + var filename = Path.GetFileName(name).Substring(namespaceName.Length); var path = Path.Combine(systemProfilesPath, filename); - using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name)) + using (var stream = _assembly.GetManifestResourceStream(name)) { var fileInfo = _fileSystem.GetFileInfo(path); @@ -514,7 +515,7 @@ namespace Emby.Dlna return new ImageStream { Format = format, - Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource) + Stream = _assembly.GetManifestResourceStream(resource) }; } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 5a7c9b617e..57ed0097a0 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -246,7 +246,7 @@ namespace Emby.Dlna.Main private async Task RegisterServerEndpoints() { - var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList(); + var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); var udn = CreateUuid(_appHost.SystemId); diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index b96fa43e50..4f9e398e90 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Direction == "out") + { continue; + } if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, null); + } } return string.Format(CommandBase, action.Name, xmlNamespace, stateString); @@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Direction == "out") + { continue; + } + if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else + { stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); + } } return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); @@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo foreach (var arg in action.ArgumentList) { if (arg.Name == "InstanceID") + { stateString += BuildArgumentXml(arg, "0"); + } else if (dictionary.ContainsKey(arg.Name)) + { stateString += BuildArgumentXml(arg, dictionary[arg.Name]); + } else + { stateString += BuildArgumentXml(arg, value.ToString()); + } } return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 9485d697b1..a8f81a3b85 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using Emby.Naming.Common; namespace Emby.Naming.TV @@ -22,7 +21,9 @@ namespace Emby.Naming.TV // There were no failed tests without this block, but to be safe, we can keep it until // the regex which require file extensions are modified so that they don't need them. if (IsDirectory) + { path += ".mp4"; + } EpisodePathParserResult result = null; @@ -35,6 +36,7 @@ namespace Emby.Naming.TV continue; } } + if (isNamed.HasValue) { if (expression.IsNamed != isNamed.Value) @@ -42,6 +44,7 @@ namespace Emby.Naming.TV continue; } } + if (isOptimistic.HasValue) { if (expression.IsOptimistic != isOptimistic.Value) @@ -191,13 +194,20 @@ namespace Emby.Naming.TV private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable expressions) { - var results = expressions - .Where(i => i.IsNamed) - .Select(i => Parse(path, i)) - .Where(i => i.Success); - - foreach (var result in results) + foreach (var i in expressions) { + if (!i.IsNamed) + { + continue; + } + + var result = Parse(path, i); + + if (!result.Success) + { + continue; + } + if (string.IsNullOrEmpty(info.SeriesName)) { info.SeriesName = result.SeriesName; @@ -208,12 +218,10 @@ namespace Emby.Naming.TV info.EndingEpsiodeNumber = result.EndingEpsiodeNumber; } - if (!string.IsNullOrEmpty(info.SeriesName)) + if (!string.IsNullOrEmpty(info.SeriesName) + && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)) { - if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue) - { - break; - } + break; } } } diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs index 6febcc2f7b..0c513ea127 100644 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity { var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty))) + foreach (var item in result.Items) { + if (item.UserId == Guid.Empty) + { + continue; + } + var user = _userManager.GetUserById(item.UserId); if (user != null) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b5a64cbddc..94d2cd5daf 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -769,7 +769,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(SessionManager); serviceCollection.AddSingleton( - new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo)); + new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this)); CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); serviceCollection.AddSingleton(CollectionManager); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 949b892265..7e50650d70 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels { foreach (var item in returnItems) { - var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None); - Task.WaitAll(task); + RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); } } @@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels } numComplete++; - double percent = numComplete; - percent /= allChannelsList.Count; - + double percent = (double)numComplete / allChannelsList.Count; progress.Report(100 * percent); } @@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels foreach (var item in result.Items) { - var folder = item as Folder; - - if (folder != null) + if (item is Folder folder) { await GetChannelItemsInternal(new InternalItemsQuery { diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 7a9b722440..4109b7ad1f 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data { list.Add(row[0].ReadGuidFromBlob()); } - catch + catch (Exception ex) { - + Logger.LogError(ex, "Error while getting user"); } } } From ed07ed44ae99d3c53a95774cdbd6254c9cb0a76e Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 1 Mar 2019 19:30:48 +0100 Subject: [PATCH 0125/6208] Simplify rating loading --- .../Localization/LocalizationManager.cs | 153 +++--------------- .../Localization/Ratings/au.csv | 8 + .../Localization/Ratings/be.csv | 6 + .../Localization/Ratings/de.csv | 10 ++ .../Localization/Ratings/ru.csv | 5 + 5 files changed, 54 insertions(+), 128 deletions(-) create mode 100644 Emby.Server.Implementations/Localization/Ratings/au.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/be.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/de.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.csv diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 31217730bf..2c59579a89 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization { const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings."; - Directory.CreateDirectory(LocalizationPath); - - var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName); - // Extract from the assembly foreach (var resource in _assembly.GetManifestResourceNames()) { @@ -74,100 +70,42 @@ namespace Emby.Server.Implementations.Localization continue; } - string filename = "ratings-" + resource.Substring(ratingsResource.Length); + string countryCode = resource.Substring(ratingsResource.Length, 2); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (existingFiles.Contains(filename)) + using (var str = _assembly.GetManifestResourceStream(resource)) + using (var reader = new StreamReader(str)) { - continue; - } - - using (var stream = _assembly.GetManifestResourceStream(resource)) - { - string target = Path.Combine(LocalizationPath, filename); - _logger.LogInformation("Extracting ratings to {0}", target); - - using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + string line; + while ((line = await reader.ReadLineAsync()) != null) { - await stream.CopyToAsync(fs); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = line.Split(','); + if (parts.Length == 2 + && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) + { + dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value })); + } +#if DEBUG + else + { + _logger.LogWarning("Misformed line in ratings file for country {CountryCode}", countryCode); + } +#endif } } - } + _logger.LogWarning("{t}", countryCode); - foreach (var file in GetRatingsFiles(LocalizationPath)) - { - await LoadRatings(file); + _allParentalRatings[countryCode] = dict; } - LoadAdditionalRatings(); - await LoadCultures(); } - private void LoadAdditionalRatings() - { - LoadRatings("au", new[] - { - new ParentalRating("AU-G", 1), - new ParentalRating("AU-PG", 5), - new ParentalRating("AU-M", 6), - new ParentalRating("AU-MA15+", 7), - new ParentalRating("AU-M15+", 8), - new ParentalRating("AU-R18+", 9), - new ParentalRating("AU-X18+", 10), - new ParentalRating("AU-RC", 11) - }); - - LoadRatings("be", new[] - { - new ParentalRating("BE-AL", 1), - new ParentalRating("BE-MG6", 2), - new ParentalRating("BE-6", 3), - new ParentalRating("BE-9", 5), - new ParentalRating("BE-12", 6), - new ParentalRating("BE-16", 8) - }); - - LoadRatings("de", new[] - { - new ParentalRating("DE-0", 1), - new ParentalRating("FSK-0", 1), - new ParentalRating("DE-6", 5), - new ParentalRating("FSK-6", 5), - new ParentalRating("DE-12", 7), - new ParentalRating("FSK-12", 7), - new ParentalRating("DE-16", 8), - new ParentalRating("FSK-16", 8), - new ParentalRating("DE-18", 9), - new ParentalRating("FSK-18", 9) - }); - - LoadRatings("ru", new[] - { - new ParentalRating("RU-0+", 1), - new ParentalRating("RU-6+", 3), - new ParentalRating("RU-12+", 7), - new ParentalRating("RU-16+", 9), - new ParentalRating("RU-18+", 10) - }); - } - - private void LoadRatings(string country, ParentalRating[] ratings) - { - _allParentalRatings[country] = ratings.ToDictionary(i => i.Name); - } - - private IEnumerable GetRatingsFiles(string directory) - => _fileSystem.GetFilePaths(directory, false) - .Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase)) - .Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase)); - - /// - /// Gets the localization path. - /// - /// The localization path. - public string LocalizationPath - => Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization"); - public string NormalizeFormKD(string text) => text.Normalize(NormalizationForm.FormKD); @@ -288,47 +226,6 @@ namespace Emby.Server.Implementations.Localization return value; } - /// - /// Loads the ratings. - /// - /// The file. - /// Dictionary{System.StringParentalRating}. - private async Task LoadRatings(string file) - { - Dictionary dict - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - using (var str = File.OpenRead(file)) - using (var reader = new StreamReader(str)) - { - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) - { - dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value })); - } -#if DEBUG - else - { - _logger.LogWarning("Misformed line in {Path}", file); - } -#endif - } - } - - var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1]; - - _allParentalRatings[countryCode] = dict; - } - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; /// diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv new file mode 100644 index 0000000000..940375e268 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -0,0 +1,8 @@ +AU-G,1 +AU-PG,5 +AU-M,6 +AU-MA15+,7 +AU-M15+,8 +AU-R18+,9 +AU-X18+,10 +AU-RC,11 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv new file mode 100644 index 0000000000..d3937caf78 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/be.csv @@ -0,0 +1,6 @@ +BE-AL,1 +BE-MG6,2 +BE-6,3 +BE-9,5 +BE-12,6 +BE-16,8 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv new file mode 100644 index 0000000000..f944a140d0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/de.csv @@ -0,0 +1,10 @@ +DE-0,1 +FSK-0,1 +DE-6,5 +FSK-6,5 +DE-12,7 +FSK-12,7 +DE-16,8 +FSK-16,8 +DE-18,9 +FSK-18,9 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv new file mode 100644 index 0000000000..1bc94affd6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv @@ -0,0 +1,5 @@ +RU-0+,1 +RU-6+,3 +RU-12+,7 +RU-16+,9 +RU-18+,10 From 5368112d903fc29a1ab3e4fa11abed08358a6cbb Mon Sep 17 00:00:00 2001 From: Lynxy Date: Fri, 1 Mar 2019 22:28:25 -0500 Subject: [PATCH 0126/6208] Correct the list of series types --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 70e5fa6406..06f6563a32 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data private static readonly HashSet _seriesTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { - "Audio", - "MusicAlbum", - "MusicVideo", + "Book", "AudioBook", - "AudioPodcast" + "Episode", + "Season" }; private bool HasSeriesFields(InternalItemsQuery query) From e823c11b46ccf0473aa72cb52cd5a3a9f977e61b Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 3 Mar 2019 08:29:23 +0100 Subject: [PATCH 0127/6208] Add certificate to https and minor cleanup --- .../ApplicationHost.cs | 26 +++++++++---------- .../HttpServer/HttpListenerHost.cs | 11 -------- .../WebSockets/WebSocketHandler.cs | 2 +- .../WebSockets/WebSocketManager.cs | 8 +++--- Jellyfin.Server/CoreAppHost.cs | 2 -- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e558b43544..05d8ee410c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -626,9 +626,12 @@ namespace Emby.Server.Implementations { options.Listen(IPAddress.Any, HttpPort); options.Listen(IPAddress.Loopback, HttpPort); - // TODO certs - options.Listen(IPAddress.Any, HttpsPort, listenOptions => { listenOptions.UseHttps(); }); - options.Listen(IPAddress.Loopback, HttpsPort, listenOptions => { listenOptions.UseHttps(); }); + + if (CertificateInfo != null) + { + options.Listen(IPAddress.Any, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); + options.Listen(IPAddress.Loopback, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); + } }) .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "jellyfin-web", "src")) .ConfigureServices(services => @@ -927,11 +930,9 @@ namespace Emby.Server.Implementations } } - protected virtual bool SupportsDualModeSockets => true; - - private X509Certificate GetCertificate(CertificateInfo info) + private X509Certificate2 GetCertificate(CertificateInfo info) { - var certificateLocation = info == null ? null : info.Path; + var certificateLocation = info?.Path; if (string.IsNullOrWhiteSpace(certificateLocation)) { @@ -1004,7 +1005,7 @@ namespace Emby.Server.Implementations return info; } - protected virtual FFMpegInfo GetFFMpegInfo() + protected FFMpegInfo GetFFMpegInfo() { return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo()) .GetFFMpegInfo(StartupOptions); @@ -1085,7 +1086,7 @@ namespace Emby.Server.Implementations /// private void SetStaticProperties() { - ((SqliteItemRepository)ItemRepository).ImageProcessor = ImageProcessor; + ItemRepository.ImageProcessor = ImageProcessor; // For now there's no real way to inject these properly BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem"); @@ -1211,15 +1212,12 @@ namespace Emby.Server.Implementations AllConcreteTypes = GetComposablePartAssemblies() .SelectMany(x => x.ExportedTypes) - .Where(type => - { - return type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType; - }) + .Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType) .ToArray(); } private CertificateInfo CertificateInfo { get; set; } - protected X509Certificate Certificate { get; private set; } + protected X509Certificate2 Certificate { get; private set; } private IEnumerable GetUrlPrefixes() { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index dbfb5e2438..263fcdbe9e 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -855,16 +855,5 @@ namespace Emby.Server.Implementations.HttpServer { Dispose(true); } - - public void StartServer(string[] urlPrefixes, IHttpListener httpListener) - { - UrlPrefixes = urlPrefixes; - - _listener = httpListener; - - _listener.WebSocketConnected = OnWebSocketConnected; - _listener.ErrorHandler = ErrorHandler; - _listener.RequestHandler = RequestHandler; - } } } diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs index 70b9e85aa9..eb18774408 100644 --- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs +++ b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs @@ -5,6 +5,6 @@ namespace Emby.Server.Implementations.WebSockets { public interface IWebSocketHandler { - Task ProcessMessage(WebSocketMessage message); + Task ProcessMessage(WebSocketMessage message, TaskCompletionSource taskCompletionSource); } } diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs index 888f2f0fce..5db2c8da73 100644 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -35,6 +35,7 @@ namespace Emby.Server.Implementations.WebSockets WebSocketReceiveResult result; var message = new List(); + // Keep listening for incoming messages, otherwise the socket closes automatically do { var buffer = WebSocket.CreateServerBuffer(BufferSize); @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.WebSockets } } - public async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource taskCompletionSource) + private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource taskCompletionSource) { var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName; var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) @@ -81,11 +82,12 @@ namespace Emby.Server.Implementations.WebSockets { try { - handler.ProcessMessage(info).ConfigureAwait(false); + handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false); } catch (Exception ex) { - _logger.LogError(ex, "{0} failed processing WebSocket message {1}", handler.GetType().Name, info.MessageType ?? string.Empty); + _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}", + handler.GetType().Name, info.MessageType ?? string.Empty); } })); diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 9e52247900..17259c7370 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -34,8 +34,6 @@ namespace Jellyfin.Server public override bool CanSelfRestart => StartupOptions.RestartPath != null; - protected override bool SupportsDualModeSockets => true; - protected override void RestartInternal() => Program.Restart(); protected override IEnumerable GetAssembliesWithPartsInternal() From d450169964680ed249f140de8422bf415b44958c Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 3 Mar 2019 08:46:17 +0100 Subject: [PATCH 0128/6208] Use EnableHttps instead of CertificateInfo --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 05d8ee410c..284900e870 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -627,7 +627,7 @@ namespace Emby.Server.Implementations options.Listen(IPAddress.Any, HttpPort); options.Listen(IPAddress.Loopback, HttpPort); - if (CertificateInfo != null) + if (EnableHttps) { options.Listen(IPAddress.Any, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); options.Listen(IPAddress.Loopback, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); From 5982cdad90d834a785afcec37efb0bfd3f4f83a9 Mon Sep 17 00:00:00 2001 From: Daniel Widrick Date: Sun, 3 Mar 2019 06:46:03 -0500 Subject: [PATCH 0129/6208] Implement SxxExx EpisodeNum Processing (#1009) **Changes** Implement and use SxxExx Episode numbering system from guide data if available. **Issues** Fixes #1008 --- Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs index 52ec7a135d..46bf6cc21c 100644 --- a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs +++ b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs @@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes ParseMovieDbSystem(reader, result); break; case "SxxExx": - // TODO - // S03E12 - reader.Skip(); + ParseSxxExxSystem(reader, result); break; default: // Handles empty string and nulls reader.Skip(); @@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes } } + public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result) + { + // S012E32 + + var value = reader.ReadElementContentAsString(); + var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase); + + if (res.Success) + { + int parsedInt; + + if (int.TryParse(res.Groups[1].Value, out parsedInt)) + { + result.Episode.Series = parsedInt; + } + + if (int.TryParse(res.Groups[2].Value, out parsedInt)) + { + result.Episode.Episode = parsedInt; + } + } + } + public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result) { // series/248841 From 1cc433eabc4813fd175b8ce63b27490663cbbcee Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 3 Mar 2019 13:54:14 +0100 Subject: [PATCH 0130/6208] Start the webhost before RunStartupTasks and fix ContentEncoding --- .../SocketSharp/WebSocketSharpRequest.cs | 25 ++++++++++++++++--- Jellyfin.Server/Program.cs | 1 - 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 24fd36062f..976f2ec065 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -408,11 +408,28 @@ namespace Emby.Server.Implementations.SocketSharp public string ContentType => request.ContentType; - private Encoding contentEncoding; - public Encoding ContentEncoding + private Encoding ContentEncoding { - get => contentEncoding ?? Encoding.GetEncoding(request.Headers[HeaderNames.ContentEncoding].ToString()); - set => contentEncoding = value; + get + { + // TODO is this necessary? + if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) + { + string postDataCharset = Headers["x-up-devcap-post-charset"]; + if (!string.IsNullOrEmpty(postDataCharset)) + { + try + { + return Encoding.GetEncoding(postDataCharset); + } + catch (ArgumentException) + { + } + } + } + + return request.GetTypedHeaders().ContentType.Encoding ?? Encoding.UTF8; + } } public Uri UrlReferrer => request.GetTypedHeaders().Referer; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index bfc50468f7..a43c3fea5c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -144,7 +144,6 @@ namespace Jellyfin.Server appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); await appHost.RunStartupTasks().ConfigureAwait(false); - appHost.Host.Run(); // TODO: read input for a stop command try From 6263b73d9c5659f2a6216dd2bea55df230ae3fcb Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 3 Mar 2019 14:30:41 +0100 Subject: [PATCH 0131/6208] Await host startup --- Emby.Server.Implementations/ApplicationHost.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 284900e870..c2e6481af7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -650,6 +650,8 @@ namespace Emby.Server.Implementations app.Use(ExecuteHttpHandlerAsync); }) .Build(); + + await Host.StartAsync(); } public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) From 040871459b572ce0dad09ae8f27938cb424b98e7 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 3 Mar 2019 14:35:54 +0100 Subject: [PATCH 0132/6208] Remove some unused references --- .../MediaReceiverRegistrar.cs | 2 -- .../ApplicationHost.cs | 3 -- .../Emby.Server.Implementations.csproj | 4 --- Emby.Server.Implementations/Startup.cs | 34 ------------------- 4 files changed, 43 deletions(-) delete mode 100644 Emby.Server.Implementations/Startup.cs diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index 9c6022b6cb..e47143c16a 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Xml; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c2e6481af7..00a5f864df 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -111,13 +111,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using ServiceStack; -using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; namespace Emby.Server.Implementations { @@ -638,7 +636,6 @@ namespace Emby.Server.Implementations { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.TryAddSingleton(); }) .Configure(app => { diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index d4db5f2fcc..39e9ed3756 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -26,11 +26,7 @@ - - - - diff --git a/Emby.Server.Implementations/Startup.cs b/Emby.Server.Implementations/Startup.cs deleted file mode 100644 index 164c7eeaa7..0000000000 --- a/Emby.Server.Implementations/Startup.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using MediaBrowser.Api; -using MediaBrowser.Controller; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations -{ - public class Startup - { - public IConfiguration Configuration { get; } - - public Startup(IConfiguration configuration) => Configuration = configuration; - - // Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } - - // Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app) - { - - } - } - -} From 6cc1bd544add65fdf7758c04655586c206675193 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 4 Mar 2019 19:31:26 +0100 Subject: [PATCH 0133/6208] Fix a logging statement --- .../SocketSharp/WebSocketSharpListener.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 5ddd316477..2df826957a 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -12,8 +12,9 @@ using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; - namespace Emby.Server.Implementations.SocketSharp +namespace Emby.Server.Implementations.SocketSharp { public class WebSocketSharpListener : IHttpListener { @@ -39,7 +40,7 @@ using Microsoft.Extensions.Logging; { var url = request.GetDisplayUrl(); - logger.LogInformation("{0} {1}. UserAgent: {2}", "WS", url, request.Headers["User-Agent"].ToString()); + logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString()); } public async Task ProcessWebSocketRequest(HttpContext ctx) From 557c4d065ddba0ccc6780c6a4c7811fe45351c88 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 4 Mar 2019 19:55:59 +0100 Subject: [PATCH 0134/6208] Review comments --- .../SocketSharp/WebSocketSharpResponse.cs | 6 +- .../MediaBrowser.Controller.csproj | 6 - MediaBrowser.sln | 374 +++++++++--------- 3 files changed, 188 insertions(+), 198 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs index c4fbaddd3e..5964107651 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -71,11 +71,7 @@ namespace Emby.Server.Implementations.SocketSharp public Stream OutputStream => _response.Body; - public bool IsClosed - { - get; - set; - } + public bool IsClosed { get; set; } public bool SendChunked { get; set; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 81e255d525..5898d8f984 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -16,12 +16,6 @@ - - - ..\..\..\..\..\usr\local\share\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.extensions\2.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.Extensions.dll - - - netstandard2.0 false diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 5edc1a25a6..b1d3fcdda8 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,187 +1,187 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDInfo", "BDInfo\BDInfo.csproj", "{88AE38DF-19D7-406F-A6A9-09527719A21E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsoMounter", "Emby.IsoMounting\IsoMounter\IsoMounter.csproj", "{9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - SharedVersion.cs = SharedVersion.cs - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.Build.0 = Release|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.Build.0 = Release|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.Build.0 = Release|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.Build.0 = Release|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.Build.0 = Release|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.Build.0 = Release|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.Build.0 = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.Build.0 = Release|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU - {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} - EndGlobalSection - GlobalSection(AutomaticVersions) = postSolution - UpdateAssemblyVersion = True - UpdateAssemblyFileVersion = True - UpdateAssemblyInfoVersion = True - AssemblyVersionSettings = None.None.None.None - AssemblyFileVersionSettings = None.None.None.None - AssemblyInfoVersionSettings = None.None.None.None - UpdatePackageVersion = False - AssemblyInfoVersionType = SettingsVersion - InheritWinAppVersionFrom = None - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.StandardHeader = $1 - $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n - $0.DotNetNamingPolicy = $2 - $2.DirectoryNamespaceAssociation = PrefixedHierarchical - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDInfo", "BDInfo\BDInfo.csproj", "{88AE38DF-19D7-406F-A6A9-09527719A21E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsoMounter", "Emby.IsoMounting\IsoMounter\IsoMounter.csproj", "{9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + SharedVersion.cs = SharedVersion.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.Build.0 = Release|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Any CPU.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Any CPU.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.Build.0 = Release|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.Build.0 = Release|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.Build.0 = Release|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU + {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} + EndGlobalSection + GlobalSection(AutomaticVersions) = postSolution + UpdateAssemblyVersion = True + UpdateAssemblyFileVersion = True + UpdateAssemblyInfoVersion = True + AssemblyVersionSettings = None.None.None.None + AssemblyFileVersionSettings = None.None.None.None + AssemblyInfoVersionSettings = None.None.None.None + UpdatePackageVersion = False + AssemblyInfoVersionType = SettingsVersion + InheritWinAppVersionFrom = None + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.StandardHeader = $1 + $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n + $0.DotNetNamingPolicy = $2 + $2.DirectoryNamespaceAssociation = PrefixedHierarchical + EndGlobalSection +EndGlobal From 9020f68ce16b8184c2b9f5cefde2b966b15fc741 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 4 Mar 2019 20:08:54 +0100 Subject: [PATCH 0135/6208] Use QueryHelpers.AddQueryString --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 263fcdbe9e..16e3c383b8 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -321,7 +321,7 @@ namespace Emby.Server.Implementations.HttpServer string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; return newQueryString.Count > 0 - ? $"{pagePathWithoutQueryString}?{string.Join("&", newQueryString.Select(value => value.Key + " = " + string.Join(", ", value.Value)))}" + ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) : pagePathWithoutQueryString; } From 0419deeec41cb7021c22a65fc4b63153075fd969 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 4 Mar 2019 20:18:35 +0100 Subject: [PATCH 0136/6208] Update LocalizationManager.cs --- Emby.Server.Implementations/Localization/LocalizationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 2c59579a89..d55b258a3d 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Localization if (parts.Length == 2 && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) { - dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value })); + dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value }); } #if DEBUG else From 0250204f14e8a004c926e1ec263723459788a029 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 4 Mar 2019 22:26:57 +0100 Subject: [PATCH 0137/6208] Expand todo --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 16e3c383b8..19b1b26e89 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -607,7 +607,7 @@ namespace Emby.Server.Implementations.HttpServer } finally { - // TODO + // TODO response closes automatically after the handler is done, but some functions rely on knowing if it's closed or not httpRes.IsClosed = true; stopWatch.Stop(); var elapsed = stopWatch.Elapsed; From 41df94115f2a475628b07aec351a381335e8f8a4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 4 Mar 2019 22:36:23 -0500 Subject: [PATCH 0138/6208] Update image overlays to use Jellyfin blue --- Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs | 2 +- Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs | 2 +- Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index 0d5a1d3c01..c72f295fdd 100644 --- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia foregroundWidth *= percent; foregroundWidth /= 100; - paint.Color = SKColor.Parse("#FF52B54B"); + paint.Color = SKColor.Parse("#FF00A4DC"); canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint); } } diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 62497da272..7f3c18bb24 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia using (var paint = new SKPaint()) { - paint.Color = SKColor.Parse("#CC52B54B"); + paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index ba712bff77..dbf935f4e7 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia using (var paint = new SKPaint()) { - paint.Color = SKColor.Parse("#CC52B54B"); + paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } From 17ca23d73b9dc440373abcde2df39e1cf7a50e35 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 07:41:41 +0100 Subject: [PATCH 0139/6208] Don't dispose the connection --- MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 5a6a0ea31e..8444125462 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -199,7 +199,8 @@ namespace MediaBrowser.Controller.Net { Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); - connection.Item1.Dispose(); + // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... + // connection.Item1.Dispose(); try { From 12df3814954a4251c30fd27ccdac89647da1a0a3 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 07:55:29 +0100 Subject: [PATCH 0140/6208] Grab content root from config and fix kestrel port bindings --- Emby.Server.Implementations/ApplicationHost.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 00a5f864df..09c40ea735 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -619,19 +619,23 @@ namespace Emby.Server.Implementations FindParts(); + string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; + if (string.IsNullOrEmpty(contentRoot)) + { + contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src"); + } + Host = new WebHostBuilder() .UseKestrel(options => { - options.Listen(IPAddress.Any, HttpPort); - options.Listen(IPAddress.Loopback, HttpPort); + options.ListenAnyIP(HttpPort); if (EnableHttps) { - options.Listen(IPAddress.Any, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); - options.Listen(IPAddress.Loopback, HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); + options.ListenAnyIP(HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); } }) - .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "jellyfin-web", "src")) + .UseContentRoot(contentRoot) .ConfigureServices(services => { services.AddResponseCompression(); @@ -655,7 +659,7 @@ namespace Emby.Server.Implementations { if (!context.WebSockets.IsWebSocketRequest) { - await next(); + await next().ConfigureAwait(false); return; } @@ -665,7 +669,7 @@ namespace Emby.Server.Implementations { if (context.WebSockets.IsWebSocketRequest) { - await next(); + await next().ConfigureAwait(false); return; } From 51648a2a21e4829a66b0bff0ef1e0e23be1c6245 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 08:05:42 +0100 Subject: [PATCH 0141/6208] Remove unused _listener --- .../HttpServer/HttpListenerHost.cs | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 19b1b26e89..293b28e74b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -33,12 +33,8 @@ namespace Emby.Server.Implementations.HttpServer public class HttpListenerHost : IHttpServer, IDisposable { private string DefaultRedirectPath { get; set; } - - private readonly ILogger _logger; public string[] UrlPrefixes { get; private set; } - private IHttpListener _listener; - public event EventHandler> WebSocketConnected; private readonly IServerConfigurationManager _config; @@ -68,7 +64,7 @@ namespace Emby.Server.Implementations.HttpServer IHttpListener socketListener) { _appHost = applicationHost; - _logger = loggerFactory.CreateLogger("HttpServer"); + Logger = loggerFactory.CreateLogger("HttpServer"); _config = config; DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; _networkManager = networkManager; @@ -85,7 +81,7 @@ namespace Emby.Server.Implementations.HttpServer public string GlobalResponse { get; set; } - protected ILogger Logger => _logger; + protected ILogger Logger { get; } public object CreateInstance(Type type) { @@ -151,7 +147,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, Logger) { OnReceive = ProcessWebSocketMessageReceived, Url = e.Url, @@ -220,11 +216,11 @@ namespace Emby.Server.Implementations.HttpServer if (logExceptionStackTrace) { - _logger.LogError(ex, "Error processing request"); + Logger.LogError(ex, "Error processing request"); } else if (logExceptionMessage) { - _logger.LogError(ex.Message); + Logger.LogError(ex.Message); } var httpRes = httpReq.Response; @@ -242,7 +238,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception errorEx) { - _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); + Logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); } } @@ -285,14 +281,6 @@ namespace Emby.Server.Implementations.HttpServer } } - - if (_listener != null) - { - _logger.LogInformation("Stopping HttpListener..."); - var task = _listener.Stop(); - Task.WaitAll(task); - _logger.LogInformation("HttpListener stopped"); - } } public static string RemoveQueryStringByKey(string url, string key) @@ -613,11 +601,11 @@ namespace Emby.Server.Implementations.HttpServer var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + Logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } else { - _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + Logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } } } @@ -630,7 +618,7 @@ namespace Emby.Server.Implementations.HttpServer var pathParts = pathInfo.TrimStart('/').Split('/'); if (pathParts.Length == 0) { - _logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); + Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); return null; } @@ -644,7 +632,7 @@ namespace Emby.Server.Implementations.HttpServer }; } - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); + Logger.LogError("Could not find handler for {PathInfo}", pathInfo); return null; } @@ -696,7 +684,7 @@ namespace Emby.Server.Implementations.HttpServer ServiceController = new ServiceController(); - _logger.LogInformation("Calling ServiceStack AppHost.Init"); + Logger.LogInformation("Calling ServiceStack AppHost.Init"); var types = services.Select(r => r.GetType()).ToArray(); @@ -704,7 +692,7 @@ namespace Emby.Server.Implementations.HttpServer ResponseFilters = new Action[] { - new ResponseFilter(_logger).FilterResponse + new ResponseFilter(Logger).FilterResponse }; } @@ -834,7 +822,7 @@ namespace Emby.Server.Implementations.HttpServer return Task.CompletedTask; } - _logger.LogDebug("Websocket message received: {0}", result.MessageType); + Logger.LogDebug("Websocket message received: {0}", result.MessageType); var tasks = _webSocketListeners.Select(i => Task.Run(async () => { @@ -844,7 +832,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception ex) { - _logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty); + Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty); } })); From 2c26517172ca2c2f1df1c83d9300ad7c66667866 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 4 Mar 2019 23:58:25 -0800 Subject: [PATCH 0142/6208] minor style fixes --- .../Cryptography/CryptographyProvider.cs | 42 +++++---- .../Library/DefaultAuthenticationProvider.cs | 39 ++++---- .../Library/UserManager.cs | 2 +- .../Cryptography/ICryptoProvider.cs | 42 ++++----- .../Cryptography/PasswordHash.cs | 92 +++++++++++-------- 5 files changed, 115 insertions(+), 102 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 3c9403ba8e..cf1ea6efa3 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -11,17 +11,21 @@ namespace Emby.Server.Implementations.Cryptography { public class CryptographyProvider : ICryptoProvider { - private HashSet SupportedHashMethods; + private HashSet _supportedHashMethods; + public string DefaultHashMethod => "PBKDF2"; - private RandomNumberGenerator rng; - private int defaultiterations = 1000; + + private RandomNumberGenerator _randomNumberGenerator; + + private int _defaultIterations = 1000; + public CryptographyProvider() { //FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one //Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 - SupportedHashMethods = new HashSet() + _supportedHashMethods = new HashSet() { "MD5" ,"System.Security.Cryptography.MD5" @@ -38,7 +42,7 @@ namespace Emby.Server.Implementations.Cryptography ,"SHA-512" ,"System.Security.Cryptography.SHA512" }; - rng = RandomNumberGenerator.Create(); + _randomNumberGenerator = RandomNumberGenerator.Create(); } public Guid GetMD5(string str) @@ -72,7 +76,7 @@ namespace Emby.Server.Implementations.Cryptography public IEnumerable GetSupportedHashMethods() { - return SupportedHashMethods; + return _supportedHashMethods; } private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) @@ -86,12 +90,13 @@ namespace Emby.Server.Implementations.Cryptography return r.GetBytes(32); } } + throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); } - public byte[] ComputeHash(string HashMethod, byte[] bytes) + public byte[] ComputeHash(string hashMethod, byte[] bytes) { - return ComputeHash(HashMethod, bytes, new byte[0]); + return ComputeHash(hashMethod, bytes, new byte[0]); } public byte[] ComputeHashWithDefaultMethod(byte[] bytes) @@ -99,15 +104,15 @@ namespace Emby.Server.Implementations.Cryptography return ComputeHash(DefaultHashMethod, bytes); } - public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) + public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { - if(HashMethod == DefaultHashMethod) + if(hashMethod == DefaultHashMethod) { - return PBKDF2(HashMethod, bytes, salt, defaultiterations); + return PBKDF2(hashMethod, bytes, salt, _defaultIterations); } - else if (SupportedHashMethods.Contains(HashMethod)) + else if (_supportedHashMethods.Contains(hashMethod)) { - using (var h = HashAlgorithm.Create(HashMethod)) + using (var h = HashAlgorithm.Create(hashMethod)) { if (salt.Length == 0) { @@ -121,21 +126,21 @@ namespace Emby.Server.Implementations.Cryptography } else { - throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); } } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) { - return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); + return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); } public byte[] ComputeHash(PasswordHash hash) { - int iterations = defaultiterations; + int iterations = _defaultIterations; if (!hash.Parameters.ContainsKey("iterations")) { - hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture)); + hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture)); } else { @@ -148,13 +153,14 @@ namespace Emby.Server.Implementations.Cryptography throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); } } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); } public byte[] GenerateSalt() { byte[] salt = new byte[64]; - rng.GetBytes(salt); + _randomNumberGenerator.GetBytes(salt); return salt; } } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 7ccdccc0ad..8f10b5a84e 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -19,18 +19,16 @@ namespace Emby.Server.Implementations.Library 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 + + // 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 public Task Authenticate(string username, string password) { throw new NotImplementedException(); } - - - //This is the verson that we need to use for local users. Because reasons. + + // This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { bool success = false; @@ -39,7 +37,7 @@ namespace Emby.Server.Implementations.Library throw new Exception("Invalid username or password"); } - //As long as jellyfin supports passwordless users, we need this little block here to accomodate + // As long as jellyfin supports passwordless users, we need this little block here to accomodate if (IsPasswordEmpty(resolvedUser, password)) { return Task.FromResult(new ProviderAuthenticationResult @@ -70,7 +68,7 @@ namespace Emby.Server.Implementations.Library if (CalculatedHashString == readyHash.Hash) { success = true; - //throw new Exception("Invalid username or password"); + // throw new Exception("Invalid username or password"); } } else @@ -78,7 +76,7 @@ namespace Emby.Server.Implementations.Library throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); } - //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); if (!success) { @@ -91,8 +89,8 @@ 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. + // 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)) @@ -121,18 +119,13 @@ namespace Emby.Server.Implementations.Library private bool IsPasswordEmpty(User user, string password) { - if (string.IsNullOrEmpty(user.Password)) - { - return string.IsNullOrEmpty(password); - } - - return false; + return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password)); } public Task ChangePassword(User user, string newPassword) { ConvertPasswordFormat(user); - //This is needed to support changing a no password user to a password user + // This is needed to support changing a no password user to a password user if (string.IsNullOrEmpty(user.Password)) { PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); @@ -184,7 +177,7 @@ namespace Emby.Server.Implementations.Library public string GetHashedString(User user, string str) { PasswordHash passwordHash; - if (String.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(user.Password)) { passwordHash = new PasswordHash(_cryptographyProvider); } @@ -196,13 +189,13 @@ namespace Emby.Server.Implementations.Library if (passwordHash.SaltBytes != null) { - //the password is modern format with PBKDF and we should take advantage of that + // the password is modern format with PBKDF and we should take advantage of that passwordHash.HashBytes = 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 + // 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))); } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 0f188ca757..57bf16364d 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.Library private readonly Func _dtoServiceFactory; private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; - + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 8accc696e3..5988112c2e 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,22 +1,22 @@ -using System; -using System.IO; -using System.Collections.Generic; - -namespace MediaBrowser.Model.Cryptography -{ - public interface ICryptoProvider - { - Guid GetMD5(string str); - byte[] ComputeMD5(Stream str); - byte[] ComputeMD5(byte[] bytes); - 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); +using System; +using System.IO; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Cryptography +{ + public interface ICryptoProvider + { + Guid GetMD5(string str); + byte[] ComputeMD5(Stream str); + byte[] ComputeMD5(byte[] bytes); + 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(); - string DefaultHashMethod { get; } - } -} + string DefaultHashMethod { get; } + } +} diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 49bd510e96..a528404045 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -6,27 +6,40 @@ 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 - //$[$=(,=)*][$[$]] + // 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 string salt; - private byte[] saltBytes; - private string hash; - private byte[] hashBytes; - public string Id { get => id; set => id = value; } - public Dictionary Parameters { get => parameters; set => parameters = value; } - public string Salt { get => salt; set => salt = value; } - public byte[] SaltBytes { get => saltBytes; set => saltBytes = value; } - public string Hash { get => hash; set => hash = value; } - public byte[] HashBytes { get => hashBytes; set => hashBytes = value; } + private string _id; + + private Dictionary _parameters = new Dictionary(); + + private string _salt; + + private byte[] _saltBytes; + + private string _hash; + + private byte[] _hashBytes; + + public string Id { get => _id; set => _id = value; } + + public Dictionary Parameters { get => _parameters; set => _parameters = value; } + + public string Salt { get => _salt; set => _salt = value; } + + public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } + + public string Hash { get => _hash; set => _hash = value; } + + public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); - id = splitted[1]; + _id = splitted[1]; if (splitted[2].Contains("=")) { foreach (string paramset in (splitted[2].Split(','))) @@ -36,7 +49,7 @@ namespace MediaBrowser.Model.Cryptography string[] fields = paramset.Split('='); if (fields.Length == 2) { - parameters.Add(fields[0], fields[1]); + _parameters.Add(fields[0], fields[1]); } else { @@ -46,32 +59,32 @@ namespace MediaBrowser.Model.Cryptography } if (splitted.Length == 5) { - salt = splitted[3]; - saltBytes = ConvertFromByteString(salt); - hash = splitted[4]; - hashBytes = ConvertFromByteString(hash); + _salt = splitted[3]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[4]; + _hashBytes = ConvertFromByteString(_hash); } else { - salt = string.Empty; - hash = splitted[3]; - hashBytes = ConvertFromByteString(hash); + _salt = string.Empty; + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); } } else { if (splitted.Length == 4) { - salt = splitted[2]; - saltBytes = ConvertFromByteString(salt); - hash = splitted[3]; - hashBytes = ConvertFromByteString(hash); + _salt = splitted[2]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); } else { - salt = string.Empty; - hash = splitted[2]; - hashBytes = ConvertFromByteString(hash); + _salt = string.Empty; + _hash = splitted[2]; + _hashBytes = ConvertFromByteString(_hash); } } @@ -80,9 +93,9 @@ namespace MediaBrowser.Model.Cryptography public PasswordHash(ICryptoProvider cryptoProvider) { - id = cryptoProvider.DefaultHashMethod; - saltBytes = cryptoProvider.GenerateSalt(); - salt = ConvertToByteString(SaltBytes); + _id = cryptoProvider.DefaultHashMethod; + _saltBytes = cryptoProvider.GenerateSalt(); + _salt = ConvertToByteString(SaltBytes); } public static byte[] ConvertFromByteString(string byteString) @@ -92,6 +105,7 @@ namespace MediaBrowser.Model.Cryptography { Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } + return Bytes.ToArray(); } @@ -103,7 +117,7 @@ namespace MediaBrowser.Model.Cryptography private string SerializeParameters() { string ReturnString = string.Empty; - foreach (var KVP in parameters) + foreach (var KVP in _parameters) { ReturnString += $",{KVP.Key}={KVP.Value}"; } @@ -118,19 +132,19 @@ namespace MediaBrowser.Model.Cryptography public override string ToString() { - string outString = "$" +id; + string outString = "$" +_id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { outString += $"${paramstring}"; } - if (!string.IsNullOrEmpty(salt)) + if (!string.IsNullOrEmpty(_salt)) { - outString += $"${salt}"; + outString += $"${_salt}"; } - outString += $"${hash}"; + outString += $"${_hash}"; return outString; } } From bc00617df7eee51a80302816bef9a797455e7780 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 10:26:43 +0100 Subject: [PATCH 0143/6208] Remove unused Brotli compressor --- Emby.Server.Implementations/ApplicationHost.cs | 7 +------ .../HttpServer/HttpResultFactory.cs | 15 +-------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 09c40ea735..deb1d3cee0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -741,7 +741,7 @@ namespace Emby.Server.Implementations ZipClient = new ZipClient(); serviceCollection.AddSingleton(ZipClient); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, CreateBrotliCompressor()); + HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer); serviceCollection.AddSingleton(HttpResultFactory); serviceCollection.AddSingleton(this); @@ -896,11 +896,6 @@ namespace Emby.Server.Implementations _serviceProvider = serviceCollection.BuildServiceProvider(); } - protected virtual IBrotliCompressor CreateBrotliCompressor() - { - return null; - } - public virtual string PackageRuntime => "netcore"; public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 52c8221f68..be50719d85 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -35,16 +35,13 @@ namespace Emby.Server.Implementations.HttpServer private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; - private IBrotliCompressor _brotliCompressor; - /// /// Initializes a new instance of the class. /// - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor) + public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; - _brotliCompressor = brotliCompressor; _logger = loggerfactory.CreateLogger("HttpResultFactory"); } @@ -350,11 +347,6 @@ namespace Emby.Server.Implementations.HttpServer private byte[] Compress(byte[] bytes, string compressionType) { - if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase)) - { - return CompressBrotli(bytes); - } - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) { return Deflate(bytes); @@ -368,11 +360,6 @@ namespace Emby.Server.Implementations.HttpServer throw new NotSupportedException(compressionType); } - private byte[] CompressBrotli(byte[] bytes) - { - return _brotliCompressor.Compress(bytes); - } - private static byte[] Deflate(byte[] bytes) { // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream From 318e0d4a2449baff806afb4ee22dc0f4021ca180 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 10:27:25 +0100 Subject: [PATCH 0144/6208] Add GetValueOrDefault dictionary extension --- Emby.Dlna/PlayTo/PlayToController.cs | 22 +++++++++---------- .../Extensions/CollectionExtensions.cs | 14 ++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 MediaBrowser.Common/Extensions/CollectionExtensions.cs diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 2da8bb8609..8d7d59249a 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -17,7 +18,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -849,13 +849,13 @@ namespace Emby.Dlna.PlayTo if (index == -1) return request; var query = url.Substring(index + 1); - Dictionary values = QueryHelpers.ParseQuery(query); + Dictionary values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); - request.DeviceProfileId = values["DeviceProfileId"].ToString(); - request.DeviceId = values["DeviceId"].ToString(); - request.MediaSourceId = values["MediaSourceId"].ToString(); - request.LiveStreamId = values["LiveStreamId"].ToString(); - request.IsDirectStream = string.Equals("true", values["Static"].ToString(), StringComparison.OrdinalIgnoreCase); + request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); + request.DeviceId = values.GetValueOrDefault("DeviceId"); + request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); + request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); + request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); @@ -869,9 +869,9 @@ namespace Emby.Dlna.PlayTo } } - private static int? GetIntValue(IReadOnlyDictionary values, string name) + private static int? GetIntValue(IReadOnlyDictionary values, string name) { - var value = values[name].ToString(); + var value = values.GetValueOrDefault(name); if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { @@ -881,9 +881,9 @@ namespace Emby.Dlna.PlayTo return null; } - private static long GetLongValue(IReadOnlyDictionary values, string name) + private static long GetLongValue(IReadOnlyDictionary values, string name) { - var value = values[name].ToString(); + var value = values.GetValueOrDefault(name); if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000000..f7c0e3cf04 --- /dev/null +++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +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) + { + dictionary.TryGetValue(key, out var ret); + return ret; + } + } +} From 942c400c19157ca437c7c885677a0a92fdd120f5 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 5 Mar 2019 17:12:56 +0100 Subject: [PATCH 0145/6208] Update README with Azure Pipelines status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d869c89789..1f635bdd25 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ GPL 2.0 License Current Release Translations -Build Status +Azure DevOps builds Docker Pull Count
Donate From 78742b8e4c658b1f02c9c040b8abb8ee5742bed4 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 19:20:28 +0100 Subject: [PATCH 0146/6208] Switch to HeaderNames instead of hardcoded strings (and other header related fixes) --- .../HttpClientManager/HttpClientManager.cs | 5 +- .../HttpServer/FileWriter.cs | 9 ++- .../HttpServer/HttpResultFactory.cs | 41 +++++----- .../HttpServer/RangeRequestWriter.cs | 7 +- .../HttpServer/ResponseFilter.cs | 7 +- .../Security/AuthorizationContext.cs | 3 +- .../HttpServer/StreamWriter.cs | 5 +- .../LiveTv/Listings/SchedulesDirect.cs | 5 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 3 +- .../SocketSharp/RequestMono.cs | 5 +- MediaBrowser.Api/Images/ImageService.cs | 3 +- MediaBrowser.Api/Library/LibraryService.cs | 3 +- MediaBrowser.Api/LiveTv/LiveTvService.cs | 15 ++-- .../BaseProgressiveStreamingService.cs | 81 ++++--------------- MediaBrowser.Common/Net/HttpRequestOptions.cs | 9 ++- 15 files changed, 82 insertions(+), 119 deletions(-) diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index ef82d1d9ee..1bebdd1637 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpClientManager { @@ -179,11 +180,11 @@ namespace Emby.Server.Implementations.HttpClientManager foreach (var header in options.RequestHeaders) { - if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase)) { request.Accept = header.Value; } - else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) { SetUserAgent(request, header.Value); hasUserAgent = true; diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 1375089e39..303db99801 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -9,6 +9,7 @@ using Emby.Server.Implementations.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -56,10 +57,10 @@ namespace Emby.Server.Implementations.HttpServer FileSystem = fileSystem; RangeHeader = rangeHeader; - Headers["Content-Type"] = contentType; + Headers[HeaderNames.ContentType] = contentType; TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers["Accept-Ranges"] = "bytes"; + Headers[HeaderNames.AcceptRanges] = "bytes"; if (string.IsNullOrWhiteSpace(rangeHeader)) { @@ -97,8 +98,8 @@ namespace Emby.Server.Implementations.HttpServer // Content-Length is the length of what we're serving, not the original content var lengthString = RangeLength.ToString(UsCulture); - var rangeString = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); - Headers["Content-Range"] = rangeString; + var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; + Headers[HeaderNames.ContentRange] = rangeString; Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index be50719d85..f4fe6b611a 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.HttpServer public object GetRedirectResult(string url) { var responseHeaders = new Dictionary(); - responseHeaders["Location"] = url; + responseHeaders[HeaderNames.Location] = url; var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect); @@ -96,9 +96,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -142,9 +142,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -186,9 +186,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -213,7 +213,7 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); } - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; return ToOptimizedResultInternal(requestContext, result, responseHeaders); } @@ -245,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer private static string GetCompressionType(IRequest request) { - var acceptEncoding = request.Headers["Accept-Encoding"].ToString(); + var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); if (string.IsNullOrEmpty(acceptEncoding)) { @@ -325,9 +325,9 @@ namespace Emby.Server.Implementations.HttpServer } content = Compress(content, requestedCompressionType); - responseHeaders["Content-Encoding"] = requestedCompressionType; + responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - responseHeaders["Vary"] = "Accept-Encoding"; + responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; var contentLength = content.Length; @@ -537,7 +537,7 @@ namespace Emby.Server.Implementations.HttpServer AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); AddAgeHeader(responseHeaders, options.DateLastModified); - var rangeHeader = requestContext.Headers["Range"]; + var rangeHeader = requestContext.Headers[HeaderNames.Range]; if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) { @@ -606,23 +606,23 @@ namespace Emby.Server.Implementations.HttpServer { if (noCache) { - responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; - responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; + responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; + responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; return; } if (cacheDuration.HasValue) { - responseHeaders["Cache-Control"] = "public, max-age=" + cacheDuration.Value.TotalSeconds; + responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; } else { - responseHeaders["Cache-Control"] = "public"; + responseHeaders[HeaderNames.CacheControl] = "public"; } if (lastModifiedDate.HasValue) { - responseHeaders["Last-Modified"] = lastModifiedDate.ToString(); + responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); } } @@ -635,7 +635,7 @@ namespace Emby.Server.Implementations.HttpServer { if (lastDateModified.HasValue) { - responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); + responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); } } @@ -693,9 +693,4 @@ namespace Emby.Server.Implementations.HttpServer } } } - - public interface IBrotliCompressor - { - byte[] Compress(byte[] content); - } } diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 8904e11d30..a0bfe5d8f9 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -66,8 +67,8 @@ namespace Emby.Server.Implementations.HttpServer this._logger = logger; ContentType = contentType; - Headers["Content-Type"] = contentType; - Headers["Accept-Ranges"] = "bytes"; + Headers[HeaderNames.ContentType] = contentType; + Headers[HeaderNames.AcceptRanges] = "bytes"; StatusCode = HttpStatusCode.PartialContent; SetRangeValues(contentLength); @@ -96,7 +97,7 @@ namespace Emby.Server.Implementations.HttpServer RangeLength = 1 + RangeEnd - RangeStart; // Content-Length is the length of what we're serving, not the original content - Headers["Content-Range"] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; + Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; if (RangeStart > 0 && SourceStream.CanSeek) { diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index ae6a6576e0..a53d9bf0b9 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Text; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -44,13 +45,13 @@ namespace Emby.Server.Implementations.HttpServer if (dto is IHasHeaders hasHeaders) { - if (!hasHeaders.Headers.ContainsKey("Server")) + if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) { - hasHeaders.Headers["Server"] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; + hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; } // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue("Content-Length", out string contentLength) + if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) && !string.IsNullOrEmpty(contentLength)) { var length = long.Parse(contentLength, UsCulture); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index cab41e65b9..276312a300 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Services; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer.Security { @@ -176,7 +177,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers["Authorization"]; + auth = httpReq.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 90354385dc..66a13e20df 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -52,7 +53,7 @@ namespace Emby.Server.Implementations.HttpServer SourceStream = source; - Headers["Content-Type"] = contentType; + Headers[HeaderNames.ContentType] = contentType; } /// @@ -69,7 +70,7 @@ namespace Emby.Server.Implementations.HttpServer SourceBytes = source; - Headers["Content-Type"] = contentType; + Headers[HeaderNames.ContentType] = contentType; } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 0bbffb824b..4137760d07 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -17,6 +17,7 @@ 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 { @@ -638,7 +639,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings #if NETSTANDARD2_0 if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - options.RequestHeaders["Accept-Encoding"] = "deflate"; + options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate"; } #endif @@ -676,7 +677,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings #if NETSTANDARD2_0 if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - options.RequestHeaders["Accept-Encoding"] = "deflate"; + options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate"; } #endif diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index fdaaf0bae7..588dcb843b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (protocol == MediaProtocol.Http) { // Use user-defined user-agent. If there isn't one, make it look like a browser. - httpHeaders["User-Agent"] = string.IsNullOrWhiteSpace(info.UserAgent) ? + httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" : info.UserAgent; } diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index f73adc5ff8..82bcf2f043 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using MediaBrowser.Model.Services; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.SocketSharp { @@ -114,9 +115,9 @@ namespace Emby.Server.Implementations.SocketSharp return form; } - public string Accept => StringValues.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"].ToString(); + public string Accept => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Accept]) ? null : request.Headers[HeaderNames.Accept].ToString(); - public string Authorization => StringValues.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"].ToString(); + public string Authorization => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]) ? null : request.Headers[HeaderNames.Authorization].ToString(); protected bool validate_form { get; set; } protected bool checked_form { get; set; } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 61db7b8d47..c25204bf2d 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Images { @@ -634,7 +635,7 @@ namespace MediaBrowser.Api.Images var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); - headers["Vary"] = "Accept"; + headers[HeaderNames.Vary] = HeaderNames.Accept; return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index d44b07256d..8eefbdf2ca 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -29,6 +29,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Library { @@ -827,7 +828,7 @@ namespace MediaBrowser.Api.Library var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty); if (!string.IsNullOrWhiteSpace(filename)) { - headers["Content-Disposition"] = "attachment; filename=\"" + filename + "\""; + headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\""; } return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 8fdd726b7b..88ed734569 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -24,6 +24,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.LiveTv { @@ -750,9 +751,10 @@ namespace MediaBrowser.Api.LiveTv throw new FileNotFoundException(); } - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType(path); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path) + }; return new ProgressiveFileCopier(_fileSystem, _streamHelper, path, outputHeaders, Logger, _environment) { @@ -772,9 +774,10 @@ namespace MediaBrowser.Api.LiveTv var directStreamProvider = liveStreamInfo; - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType("file." + request.Container); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container) + }; return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger, _environment) { diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 3e74d59db7..fc81e532d8 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Playback.Progressive { @@ -154,7 +155,7 @@ namespace MediaBrowser.Api.Playback.Progressive var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); // TODO: Don't hardcode this - outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts"); + outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts"); return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) { @@ -196,9 +197,11 @@ namespace MediaBrowser.Api.Playback.Progressive { if (state.MediaSource.IsInfiniteStream) { - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = contentType + }; - outputHeaders["Content-Type"] = contentType; return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) { @@ -298,16 +301,16 @@ namespace MediaBrowser.Api.Playback.Progressive if (trySupportSeek) { - if (!string.IsNullOrWhiteSpace(Request.QueryString["Range"])) + if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range])) { - options.RequestHeaders["Range"] = Request.QueryString["Range"]; + options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range]; } } var response = await HttpClient.GetResponse(options).ConfigureAwait(false); if (trySupportSeek) { - foreach (var name in new[] { "Content-Range", "Accept-Ranges" }) + foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges }) { var val = response.Headers[name]; if (!string.IsNullOrWhiteSpace(val)) @@ -318,14 +321,7 @@ namespace MediaBrowser.Api.Playback.Progressive } else { - responseHeaders["Accept-Ranges"] = "none"; - } - - // Seeing cases of -1 here - if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) - { - // TODO - //responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); + responseHeaders[HeaderNames.AcceptRanges] = "none"; } if (isHeadRequest) @@ -338,7 +334,7 @@ namespace MediaBrowser.Api.Playback.Progressive var result = new StaticRemoteStreamWriter(response); - result.Headers["Content-Type"] = response.ContentType; + result.Headers[HeaderNames.ContentType] = response.ContentType; // Add the response headers to the result object foreach (var header in responseHeaders) @@ -362,42 +358,14 @@ namespace MediaBrowser.Api.Playback.Progressive // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; - responseHeaders["Accept-Ranges"] = "none"; + responseHeaders[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); - // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response - // What we really want to do is hunt that down and remove that - var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null; - - if (contentLength.HasValue) - { - responseHeaders["Content-Length"] = contentLength.Value.ToString(UsCulture); - } - // Headers only if (isHeadRequest) { - var streamResult = ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders); - - var hasHeaders = streamResult as IHasHeaders; - if (hasHeaders != null) - { - // TODO - //if (contentLength.HasValue) - //{ - // hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture); - //} - //else - //{ - if (hasHeaders.Headers.ContainsKey("Content-Length")) - { - hasHeaders.Headers.Remove("Content-Length"); - } - //} - } - - return streamResult; + return ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders); } var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); @@ -416,9 +384,11 @@ namespace MediaBrowser.Api.Playback.Progressive state.Dispose(); } - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = contentType + }; - outputHeaders["Content-Type"] = contentType; // Add the response headers to the result object foreach (var item in responseHeaders) @@ -433,22 +403,5 @@ namespace MediaBrowser.Api.Playback.Progressive transcodingLock.Release(); } } - - /// - /// Gets the length of the estimated content. - /// - /// The state. - /// System.Nullable{System.Int64}. - private long? GetEstimatedContentLength(StreamState state) - { - var totalBitrate = state.TotalOutputBitrate ?? 0; - - if (totalBitrate > 0 && state.RunTimeTicks.HasValue) - { - return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); - } - - return null; - } } } diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index dadac5e03d..bea178517b 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Common.Net { @@ -24,8 +25,8 @@ namespace MediaBrowser.Common.Net /// The accept header. public string AcceptHeader { - get => GetHeaderValue("Accept"); - set => RequestHeaders["Accept"] = value; + get => GetHeaderValue(HeaderNames.Accept); + set => RequestHeaders[HeaderNames.Accept] = value; } /// /// Gets or sets the cancellation token. @@ -45,8 +46,8 @@ namespace MediaBrowser.Common.Net /// The user agent. public string UserAgent { - get => GetHeaderValue("User-Agent"); - set => RequestHeaders["User-Agent"] = value; + get => GetHeaderValue(HeaderNames.UserAgent); + set => RequestHeaders[HeaderNames.UserAgent] = value; } /// From df92df7bd63f9fd19ed195f668b45d52d6bd3abc Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 19:22:41 +0100 Subject: [PATCH 0147/6208] Remove BOM --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.sln | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 94a7e58cb5..bd670df527 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,4 +1,4 @@ - + jellyfin diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 5898d8f984..01893f1b55 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors diff --git a/MediaBrowser.sln b/MediaBrowser.sln index b1d3fcdda8..e43a582242 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.3 From 9a4a01fb0e94d8ffbc00f5932f284f7faeb0ee69 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 19:32:22 +0100 Subject: [PATCH 0148/6208] Fix DI in FileWriter.TransmitFile --- .../ApplicationHost.cs | 2 +- .../HttpServer/FileWriter.cs | 30 ++++++++----------- .../HttpServer/HttpResultFactory.cs | 6 ++-- .../SocketSharp/WebSocketSharpResponse.cs | 2 -- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index deb1d3cee0..93147539c6 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -741,7 +741,7 @@ namespace Emby.Server.Implementations ZipClient = new ZipClient(); serviceCollection.AddSingleton(ZipClient); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer); + HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper); serviceCollection.AddSingleton(HttpResultFactory); serviceCollection.AddSingleton(this); diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 303db99801..c4a170eaae 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -15,6 +15,7 @@ namespace Emby.Server.Implementations.HttpServer { public class FileWriter : IHttpResult { + private readonly IStreamHelper _streamHelper; private ILogger Logger { get; set; } public IFileSystem FileSystem { get; } @@ -45,13 +46,15 @@ namespace Emby.Server.Implementations.HttpServer public string Path { get; set; } - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem) + public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException(nameof(contentType)); } + _streamHelper = streamHelper; + Path = path; Logger = logger; FileSystem = fileSystem; @@ -147,8 +150,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private string[] SkipLogExtensions = new string[] - { + private readonly string[] SkipLogExtensions = { ".js", ".html", ".css" @@ -165,8 +167,10 @@ namespace Emby.Server.Implementations.HttpServer } var path = Path; + var offset = RangeStart; + var count = RangeLength; - if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)) + if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) { var extension = System.IO.Path.GetExtension(path); @@ -175,20 +179,15 @@ namespace Emby.Server.Implementations.HttpServer Logger.LogDebug("Transmit file {0}", path); } - //var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0; - // TODO not DI friendly lol - await response.TransmitFile(path, 0, 0, FileShare, FileSystem, new StreamHelper(), cancellationToken).ConfigureAwait(false); - return; + offset = 0; + count = 0; } - // TODO not DI friendly lol - await response.TransmitFile(path, RangeStart, RangeLength, FileShare, FileSystem, new StreamHelper(), cancellationToken).ConfigureAwait(false); + + await response.TransmitFile(path, offset, count, FileShare, FileSystem, _streamHelper, cancellationToken).ConfigureAwait(false); } finally { - if (OnComplete != null) - { - OnComplete(); - } + OnComplete?.Invoke(); } } @@ -205,8 +204,5 @@ namespace Emby.Server.Implementations.HttpServer get => (HttpStatusCode)Status; set => Status = (int)value; } - - public string StatusDescription { get; set; } - } } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index f4fe6b611a..4632658626 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -34,14 +34,16 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; + private readonly IStreamHelper _streamHelper; /// /// Initializes a new instance of the class. /// - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; + _streamHelper = streamHelper; _logger = loggerfactory.CreateLogger("HttpResultFactory"); } @@ -541,7 +543,7 @@ namespace Emby.Server.Implementations.HttpServer if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) + var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) { OnComplete = options.OnComplete, OnError = options.OnError, diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs index 5964107651..a7e3e6c700 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -78,8 +78,6 @@ namespace Emby.Server.Implementations.SocketSharp const int StreamCopyToBufferSize = 81920; public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) { - // TODO - // return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); //if (count <= 0) From 913e80fd5527775ea71d6f9606f49c5494cb7b63 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 20:35:07 +0100 Subject: [PATCH 0149/6208] Add ProcessWebSocketRequest to IHttpListener --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 5 ++--- Emby.Server.Implementations/HttpServer/IHttpListener.cs | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 293b28e74b..d1b1b5b242 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -756,11 +756,10 @@ namespace Emby.Server.Implementations.HttpServer public Task ProcessWebSocketRequest(HttpContext context) { - // TODO - return ((WebSocketSharpListener)_socketListener).ProcessWebSocketRequest(context); + return _socketListener.ProcessWebSocketRequest(context); } - //TODO Add Jellyfin Route Path Normalizer + //TODO Add Jellyfin Route Path Normalizer private static string NormalizeEmbyRoutePath(string path) { if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs index 1ef65d9d73..005656d2c1 100644 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer { @@ -30,5 +31,7 @@ namespace Emby.Server.Implementations.HttpServer /// Stops this instance. /// Task Stop(); + + Task ProcessWebSocketRequest(HttpContext ctx); } } From 446f9bf81fa3ac737acda71d57fdc7289c227cef Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 5 Mar 2019 20:48:04 +0100 Subject: [PATCH 0150/6208] Remove more Content-Length references --- .../HttpServer/FileWriter.cs | 4 +-- .../HttpServer/RangeRequestWriter.cs | 1 - .../Services/QueryParamCollection.cs | 26 +++---------------- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index c4a170eaae..78e2015b1e 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -99,12 +99,10 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(UsCulture); var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = rangeString; - Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); + Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString); } /// diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index a0bfe5d8f9..449159834a 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -96,7 +96,6 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; - // Content-Length is the length of what we're serving, not the original content Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; if (RangeStart > 0 && SourceStream.CanSeek) diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 4631a3b633..7708db00a8 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -25,16 +25,12 @@ namespace MediaBrowser.Model.Services /// /// Adds a new query parameter. /// - public virtual void Add(string key, string value) + public void Add(string key, string value) { - if (string.Equals(key, "content-length", StringComparison.OrdinalIgnoreCase)) - { - return; - } Add(new NameValuePair(key, value)); } - public virtual void Set(string key, string value) + private void Set(string key, string value) { if (string.IsNullOrEmpty(value)) { @@ -62,7 +58,7 @@ namespace MediaBrowser.Model.Services Add(key, value); } - public string Get(string name) + private string Get(string name) { var stringComparison = GetStringComparison(); @@ -77,7 +73,7 @@ namespace MediaBrowser.Model.Services return null; } - public virtual List GetItems(string name) + private List GetItems(string name) { var stringComparison = GetStringComparison(); @@ -111,20 +107,6 @@ namespace MediaBrowser.Model.Services return list; } - public Dictionary ToDictionary() - { - var stringComparer = GetStringComparer(); - - var headers = new Dictionary(stringComparer); - - foreach (var pair in this) - { - headers[pair.Name] = pair.Value; - } - - return headers; - } - public IEnumerable Keys { get From 20775116f76b12bf77672fa37c4ea5f82b69f157 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Fri, 8 Feb 2019 13:35:26 +0000 Subject: [PATCH 0151/6208] Reworked FFmpeg path discovery and always display to user 1) Reworked FFmpeg and FFprobe path discovery (CLI switch, Custom xml, system $PATH, UI update trigger). Removed FFMpeg folder from Emby.Server.Implementations. All path discovery now in MediaEncoder. 2) Always display FFmpeg path to user in Transcode page. 3) Allow user to remove a Custome FFmpeg path and return to using system $PATH (or --ffmpeg if available). 4) Remove unused code associated with 'prebuilt' FFmpeg. 5) Much improved logging during path discovery. --- .../ApplicationHost.cs | 72 +-- .../FFMpeg/FFMpegInfo.cs | 24 - .../FFMpeg/FFMpegInstallInfo.cs | 17 - .../FFMpeg/FFMpegLoader.cs | 132 ----- .../Encoder/EncoderValidator.cs | 2 +- .../Encoder/MediaEncoder.cs | 509 +++++++++--------- .../Configuration/EncodingOptions.cs | 3 +- 7 files changed, 250 insertions(+), 509 deletions(-) delete mode 100644 Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs delete mode 100644 Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs delete mode 100644 Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 94d2cd5daf..2c0d0e7469 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Dto; -using Emby.Server.Implementations.FFMpeg; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; @@ -792,7 +791,8 @@ namespace Emby.Server.Implementations ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); - RegisterMediaEncoder(serviceCollection); + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(LoggerFactory, JsonSerializer, StartupOptions.FFmpegPath, StartupOptions.FFprobePath, ServerConfigurationManager, FileSystemManager, () => SubtitleEncoder, () => MediaSourceManager, ProcessFactory, 5000); + serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); serviceCollection.AddSingleton(EncodingManager); @@ -908,83 +908,25 @@ namespace Emby.Server.Implementations return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); } - protected virtual FFMpegInstallInfo GetFfmpegInstallInfo() - { - var info = new FFMpegInstallInfo(); - - // Windows builds: http://ffmpeg.zeranoe.com/builds/ - // Linux builds: http://johnvansickle.com/ffmpeg/ - // OS X builds: http://ffmpegmac.net/ - // OS X x64: http://www.evermeet.cx/ffmpeg/ - - if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) - { - info.FFMpegFilename = "ffmpeg.exe"; - info.FFProbeFilename = "ffprobe.exe"; - info.Version = "20170308"; - info.ArchiveType = "7z"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - - return info; - } - - protected virtual FFMpegInfo GetFFMpegInfo() - { - return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo()) - .GetFFMpegInfo(StartupOptions); - } - /// /// Registers the media encoder. /// /// Task. - private void RegisterMediaEncoder(IServiceCollection serviceCollection) + private void RegisterMediaEncoder(IAssemblyInfo assemblyInfo) { - string encoderPath = null; - string probePath = null; - - var info = GetFFMpegInfo(); - - encoderPath = info.EncoderPath; - probePath = info.ProbePath; - var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase); - - var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( LoggerFactory, JsonSerializer, - encoderPath, - probePath, - hasExternalEncoder, + StartupOptions.FFmpegPath, + StartupOptions.FFprobePath, ServerConfigurationManager, FileSystemManager, - LiveTvManager, - IsoManager, - LibraryManager, - ChannelManager, - SessionManager, () => SubtitleEncoder, () => MediaSourceManager, - HttpClient, - ZipClient, ProcessFactory, 5000); - MediaEncoder = mediaEncoder; - serviceCollection.AddSingleton(MediaEncoder); + RegisterSingleInstance(MediaEncoder); } /// diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs deleted file mode 100644 index 60cd7b3d72..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - /// - /// Class FFMpegInfo - /// - public class FFMpegInfo - { - /// - /// Gets or sets the path. - /// - /// The path. - public string EncoderPath { get; set; } - /// - /// Gets or sets the probe path. - /// - /// The probe path. - public string ProbePath { get; set; } - /// - /// Gets or sets the version. - /// - /// The version. - public string Version { get; set; } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs deleted file mode 100644 index fa9cb5e01b..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegInstallInfo - { - public string Version { get; set; } - public string FFMpegFilename { get; set; } - public string FFProbeFilename { get; set; } - public string ArchiveType { get; set; } - - public FFMpegInstallInfo() - { - Version = "Path"; - FFMpegFilename = "ffmpeg"; - FFProbeFilename = "ffprobe"; - } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs deleted file mode 100644 index bbf51dd246..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegLoader - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly FFMpegInstallInfo _ffmpegInstallInfo; - - public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - _ffmpegInstallInfo = ffmpegInstallInfo; - } - - public FFMpegInfo GetFFMpegInfo(IStartupOptions options) - { - var customffMpegPath = options.FFmpegPath; - var customffProbePath = options.FFprobePath; - - if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath)) - { - return new FFMpegInfo - { - ProbePath = customffProbePath, - EncoderPath = customffMpegPath, - Version = "external" - }; - } - - var downloadInfo = _ffmpegInstallInfo; - - var prebuiltFolder = _appPaths.ProgramSystemPath; - var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename); - var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename); - if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe)) - { - return new FFMpegInfo - { - ProbePath = prebuiltffprobe, - EncoderPath = prebuiltffmpeg, - Version = "external" - }; - } - - var version = downloadInfo.Version; - - if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase)) - { - return new FFMpegInfo(); - } - - var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg"); - var versionedDirectoryPath = Path.Combine(rootEncoderPath, version); - - var info = new FFMpegInfo - { - ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename), - EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename), - Version = version - }; - - Directory.CreateDirectory(versionedDirectoryPath); - - var excludeFromDeletions = new List { versionedDirectoryPath }; - - if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath)) - { - // ffmpeg not present. See if there's an older version we can start with - var existingVersion = GetExistingVersion(info, rootEncoderPath); - - // No older version. Need to download and block until complete - if (existingVersion == null) - { - return new FFMpegInfo(); - } - else - { - info = existingVersion; - versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); - excludeFromDeletions.Add(versionedDirectoryPath); - } - } - - // Allow just one of these to be overridden, if desired. - if (!string.IsNullOrWhiteSpace(customffMpegPath)) - { - info.EncoderPath = customffMpegPath; - } - if (!string.IsNullOrWhiteSpace(customffProbePath)) - { - info.ProbePath = customffProbePath; - } - - return info; - } - - private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath) - { - var encoderFilename = Path.GetFileName(info.EncoderPath); - var probeFilename = Path.GetFileName(info.ProbePath); - - foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath)) - { - var allFiles = _fileSystem.GetFilePaths(directory, true).ToList(); - - var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase)); - var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrWhiteSpace(encoder) && - !string.IsNullOrWhiteSpace(probe)) - { - return new FFMpegInfo - { - EncoderPath = encoder, - ProbePath = probe, - Version = Path.GetFileName(Path.GetDirectoryName(probe)) - }; - } - } - - return null; - } - } -} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f725d2c019..1eeea87a06 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _processFactory = processFactory; } - public (IEnumerable decoders, IEnumerable encoders) Validate(string encoderPath) + public (IEnumerable decoders, IEnumerable encoders) GetAvailableCoders(string encoderPath) { _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f29c06b4c..36d72cad90 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -7,13 +7,9 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; @@ -32,323 +28,288 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// - /// The _logger + /// Gets the encoder path. /// + /// The encoder path. + public string EncoderPath => FFmpegPath; + + /// + /// External: path supplied via command line + /// Custom: coming from UI or config/encoding.xml file + /// System: FFmpeg found in system $PATH + /// null: No FFmpeg found + /// + public string EncoderLocationType { get; private set; } + private readonly ILogger _logger; - - /// - /// Gets the json serializer. - /// - /// The json serializer. private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _thumbnail resource pool - /// - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); - - public string FFMpegPath { get; private set; } - - public string FFProbePath { get; private set; } - + private string FFmpegPath { get; set; } + private string FFprobePath { get; set; } protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; - protected readonly ILiveTvManager LiveTvManager; - protected readonly IIsoManager IsoManager; - protected readonly ILibraryManager LibraryManager; - protected readonly IChannelManager ChannelManager; - protected readonly ISessionManager SessionManager; protected readonly Func SubtitleEncoder; protected readonly Func MediaSourceManager; - private readonly IHttpClient _httpClient; - private readonly IZipClient _zipClient; private readonly IProcessFactory _processFactory; - - private readonly List _runningProcesses = new List(); - private readonly bool _hasExternalEncoder; - private readonly string _originalFFMpegPath; - private readonly string _originalFFProbePath; private readonly int DefaultImageExtractionTimeoutMs; + private readonly string StartupOptionFFmpegPath; + private readonly string StartupOptionFFprobePath; + + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); + private readonly List _runningProcesses = new List(); public MediaEncoder( ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, - string ffMpegPath, - string ffProbePath, - bool hasExternalEncoder, + string startupOptionsFFmpegPath, + string startupOptionsFFprobePath, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - ILiveTvManager liveTvManager, - IIsoManager isoManager, - ILibraryManager libraryManager, - IChannelManager channelManager, - ISessionManager sessionManager, Func subtitleEncoder, Func mediaSourceManager, - IHttpClient httpClient, - IZipClient zipClient, IProcessFactory processFactory, int defaultImageExtractionTimeoutMs) { _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _jsonSerializer = jsonSerializer; + StartupOptionFFmpegPath = startupOptionsFFmpegPath; + StartupOptionFFprobePath = startupOptionsFFprobePath; ConfigurationManager = configurationManager; FileSystem = fileSystem; - LiveTvManager = liveTvManager; - IsoManager = isoManager; - LibraryManager = libraryManager; - ChannelManager = channelManager; - SessionManager = sessionManager; SubtitleEncoder = subtitleEncoder; - MediaSourceManager = mediaSourceManager; - _httpClient = httpClient; - _zipClient = zipClient; _processFactory = processFactory; DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; - FFProbePath = ffProbePath; - FFMpegPath = ffMpegPath; - _originalFFProbePath = ffProbePath; - _originalFFMpegPath = ffMpegPath; - _hasExternalEncoder = hasExternalEncoder; } - public string EncoderLocationType + /// + /// Run at startup or if the user removes a Custom path from transcode page. + /// Sets global variables FFmpegPath and EncoderLocationType. + /// If startup options --ffprobe is given then FFprobePath is set too. + /// + public void Init() { - get + // 1) If given, use the --ffmpeg CLI switch + if (ValidatePathFFmpeg("From CLI Switch", StartupOptionFFmpegPath)) { - if (_hasExternalEncoder) + _logger.LogInformation("FFmpeg: Using path from command line switch --ffmpeg"); + EncoderLocationType = "External"; + } + + // 2) Try Custom path stroed in config/encoding xml file under tag + else if (ValidatePathFFmpeg("From Config File", ConfigurationManager.GetConfiguration("encoding").EncoderAppPathCustom)) + { + _logger.LogInformation("FFmpeg: Using path from config/encoding.xml file"); + EncoderLocationType = "Custom"; + } + + // 3) Search system $PATH environment variable for valid FFmpeg + else if (ValidatePathFFmpeg("From $PATH", ExistsOnSystemPath("ffmpeg"))) + { + _logger.LogInformation("FFmpeg: Using system $PATH for FFmpeg"); + EncoderLocationType = "System"; + } + else + { + _logger.LogError("FFmpeg: No suitable executable found"); + FFmpegPath = null; + EncoderLocationType = null; + } + + // If given, use the --ffprobe CLI switch + if (ValidatePathFFprobe("CLI Switch", StartupOptionFFprobePath)) + { + _logger.LogInformation("FFprobe: Using path from command line switch --ffprobe"); + } + else + { + // FFprobe path from command line is no good, so set to null and let ReInit() try + // and set using the FFmpeg path. + FFprobePath = null; + } + + ReInit(); + } + + /// + /// Writes the currently used FFmpeg to config/encoding.xml file. + /// Sets the FFprobe path if not currently set. + /// Interrogates the FFmpeg tool to identify what encoders/decodres are available. + /// + private void ReInit() + { + // Write the FFmpeg path to the config/encoding.xml file so it appears in UI + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPath = FFmpegPath ?? string.Empty; + ConfigurationManager.SaveConfiguration("encoding", config); + + // Only if mpeg path is set, try and set path to probe + if (FFmpegPath != null) + { + // Probe would be null here if no valid --ffprobe path was given + // at startup, or we're performing ReInit following mpeg path update from UI + if (FFprobePath == null) { - return "External"; + // Use the mpeg path to create a probe path + if (ValidatePathFFprobe("Copied from FFmpeg:", GetProbePathFromEncoderPath(FFmpegPath))) + { + _logger.LogInformation("FFprobe: Using FFprobe in same folders as FFmpeg"); + } + else + { + _logger.LogError("FFprobe: No suitable executable found"); + } } - if (string.IsNullOrWhiteSpace(FFMpegPath)) - { - return null; - } + // Interrogate to understand what coders it supports + var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath); - if (IsSystemInstalledPath(FFMpegPath)) - { - return "System"; - } + SetAvailableDecoders(result.decoders); + SetAvailableEncoders(result.encoders); + } - return "Custom"; + // Stamp FFmpeg paths to the log file + LogPaths(); + } + + /// + /// Triggered from the Settings > Trascoding UI page when users sumits Custom FFmpeg path to use. + /// + /// + /// + public void UpdateEncoderPath(string path, string pathType) + { + _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); + + if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("Unexpected pathType value"); + } + else + { + if (string.IsNullOrWhiteSpace(path)) + { + // User had cleared the cutom path in UI. Clear the Custom config + // setting and peform full Init to relook any CLI switches and system $PATH + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathCustom = string.Empty; + ConfigurationManager.SaveConfiguration("encoding", config); + + Init(); + } + else if (!File.Exists(path) && !Directory.Exists(path)) + { + // Given path is neither file or folder + throw new ResourceNotFoundException(); + } + else + { + // Supplied path could be either file path or folder path. + // Resolve down to file path and validate + path = GetEncoderPath(path); + + if (path == null) + { + throw new ResourceNotFoundException("FFmpeg not found"); + } + else if (!ValidatePathFFmpeg("New From UI", path)) + { + throw new ResourceNotFoundException("Failed validation checks. Version 4.0 or greater is required"); + } + else + { + EncoderLocationType = "Custom"; + + // Write the validated mpeg path to the xml as + // This ensures its not lost on new startup + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathCustom = FFmpegPath; + ConfigurationManager.SaveConfiguration("encoding", config); + + FFprobePath = null; // Clear probe path so it gets relooked in ReInit() + + ReInit(); + } + } } } - private bool IsSystemInstalledPath(string path) + private bool ValidatePath(string type, string path) { - if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) + if (!string.IsNullOrEmpty(path)) { + if (File.Exists(path)) + { + var valid = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); + + if (valid == true) + { + return true; + } + else + { + _logger.LogError("{0}: Failed validation checks. Version 4.0 or greater is required: {1}", type, path); + } + } + else + { + _logger.LogError("{0}: File not found: {1}", type, path); + } + } + + return false; + } + + private bool ValidatePathFFmpeg(string comment, string path) + { + if (ValidatePath("FFmpeg: " + comment, path) == true) + { + FFmpegPath = path; return true; } return false; } - public void Init() + private bool ValidatePathFFprobe(string comment, string path) { - InitPaths(); - - if (!string.IsNullOrWhiteSpace(FFMpegPath)) + if (ValidatePath("FFprobe: " + comment, path) == true) { - var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath); - - SetAvailableDecoders(result.decoders); - SetAvailableEncoders(result.encoders); + FFprobePath = path; + return true; } + + return false; } - private void InitPaths() + private string GetEncoderPath(string path) { - ConfigureEncoderPaths(); - - if (_hasExternalEncoder) + if (Directory.Exists(path)) { - LogPaths(); - return; + return GetEncoderPathFromDirectory(path); } - // If the path was passed in, save it into config now. - var encodingOptions = GetEncodingOptions(); - var appPath = encodingOptions.EncoderAppPath; - - var valueToSave = FFMpegPath; - - if (!string.IsNullOrWhiteSpace(valueToSave)) + if (File.Exists(path)) { - // if using system variable, don't save this. - if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder) - { - valueToSave = null; - } + return path; } - if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal)) - { - encodingOptions.EncoderAppPath = valueToSave; - ConfigurationManager.SaveConfiguration("encoding", encodingOptions); - } + return null; } - public void UpdateEncoderPath(string path, string pathType) + private string GetEncoderPathFromDirectory(string path) { - if (_hasExternalEncoder) + try { - return; + var files = FileSystem.GetFilePaths(path); + + var excludeExtensions = new[] { ".c" }; + + return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); } - - _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); - - Tuple newPaths; - - if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase)) + catch (Exception) { - path = "ffmpeg"; - - newPaths = TestForInstalledVersions(); + // Trap all exceptions, like DirNotExists, and return null + return null; } - else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (!File.Exists(path) && !Directory.Exists(path)) - { - throw new ResourceNotFoundException(); - } - newPaths = GetEncoderPaths(path); - } - else - { - throw new ArgumentException("Unexpected pathType value"); - } - - if (string.IsNullOrWhiteSpace(newPaths.Item1)) - { - throw new ResourceNotFoundException("ffmpeg not found"); - } - if (string.IsNullOrWhiteSpace(newPaths.Item2)) - { - throw new ResourceNotFoundException("ffprobe not found"); - } - - path = newPaths.Item1; - - if (!ValidateVersion(path, true)) - { - throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required."); - } - - var config = GetEncodingOptions(); - config.EncoderAppPath = path; - ConfigurationManager.SaveConfiguration("encoding", config); - - Init(); - } - - private bool ValidateVersion(string path, bool logOutput) - { - return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput); - } - - private void ConfigureEncoderPaths() - { - if (_hasExternalEncoder) - { - return; - } - - var appPath = GetEncodingOptions().EncoderAppPath; - - if (string.IsNullOrWhiteSpace(appPath)) - { - appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); - } - - var newPaths = GetEncoderPaths(appPath); - if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2) || IsSystemInstalledPath(appPath)) - { - newPaths = TestForInstalledVersions(); - } - - if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2)) - { - FFMpegPath = newPaths.Item1; - FFProbePath = newPaths.Item2; - } - - LogPaths(); - } - - private Tuple GetEncoderPaths(string configuredPath) - { - var appPath = configuredPath; - - if (!string.IsNullOrWhiteSpace(appPath)) - { - if (Directory.Exists(appPath)) - { - return GetPathsFromDirectory(appPath); - } - - if (File.Exists(appPath)) - { - return new Tuple(appPath, GetProbePathFromEncoderPath(appPath)); - } - } - - return new Tuple(null, null); - } - - private Tuple TestForInstalledVersions() - { - string encoderPath = null; - string probePath = null; - - if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true)) - { - encoderPath = _originalFFMpegPath; - probePath = _originalFFProbePath; - } - - if (string.IsNullOrWhiteSpace(encoderPath)) - { - if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false)) - { - encoderPath = "ffmpeg"; - probePath = "ffprobe"; - } - } - - return new Tuple(encoderPath, probePath); - } - - private Tuple GetPathsFromDirectory(string path) - { - // Since we can't predict the file extension, first try directly within the folder - // If that doesn't pan out, then do a recursive search - var files = FileSystem.GetFilePaths(path); - - var excludeExtensions = new[] { ".c" }; - - var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - - if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) - { - files = FileSystem.GetFilePaths(path, true); - - ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - - if (!string.IsNullOrWhiteSpace(ffmpegPath)) - { - ffprobePath = GetProbePathFromEncoderPath(ffmpegPath); - } - } - - return new Tuple(ffmpegPath, ffprobePath); } private string GetProbePathFromEncoderPath(string appPath) @@ -357,15 +318,31 @@ namespace MediaBrowser.MediaEncoding.Encoder .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); } - private void LogPaths() + /// + /// Search the system $PATH environment variable looking for given filename. + /// + /// + /// + private string ExistsOnSystemPath(string fileName) { - _logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found"); - _logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found"); + var values = Environment.GetEnvironmentVariable("PATH"); + + foreach (var path in values.Split(Path.PathSeparator)) + { + var candidatePath = GetEncoderPathFromDirectory(path); + + if (ValidatePath("Found on PATH", candidatePath)) + { + return candidatePath; + } + } + return null; } - private EncodingOptions GetEncodingOptions() + private void LogPaths() { - return ConfigurationManager.GetConfiguration("encoding"); + _logger.LogInformation("FFMpeg: {0}", FFmpegPath ?? "not found"); + _logger.LogInformation("FFProbe: {0}", FFprobePath ?? "not found"); } private List _encoders = new List(); @@ -412,12 +389,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// - /// Gets the encoder path. - /// - /// The encoder path. - public string EncoderPath => FFMpegPath; - /// /// Gets the media info. /// @@ -489,7 +460,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - FileName = FFProbePath, + FileName = FFprobePath, Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), IsHidden = true, @@ -691,7 +662,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -814,7 +785,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 8584bd3ddb..ff697437ac 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -8,7 +8,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } - public string EncoderAppPath { get; set; } + public string EncoderAppPathCustom { get; set; } // FFmpeg path as set by the user via the UI + public string EncoderAppPath { get; set; } // The current FFmpeg path being used by the system public string VaapiDevice { get; set; } public int H264Crf { get; set; } public string H264Preset { get; set; } From ed69e690b89e6a3e6e22bbf448af08a25c38e71b Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Tue, 12 Feb 2019 22:05:42 +0000 Subject: [PATCH 0152/6208] Review comments Address review comments from JustAMan, Bond-009 and cvium. --- .../ApplicationHost.cs | 35 +-- .../MediaEncoding/IMediaEncoder.cs | 3 +- .../Encoder/EncoderValidator.cs | 8 + .../Encoder/MediaEncoder.cs | 247 ++++++++---------- MediaBrowser.Model/System/SystemInfo.cs | 17 +- 5 files changed, 150 insertions(+), 160 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 2c0d0e7469..dd29f2adec 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -791,7 +791,17 @@ namespace Emby.Server.Implementations ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); - MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(LoggerFactory, JsonSerializer, StartupOptions.FFmpegPath, StartupOptions.FFprobePath, ServerConfigurationManager, FileSystemManager, () => SubtitleEncoder, () => MediaSourceManager, ProcessFactory, 5000); + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( + LoggerFactory, + JsonSerializer, + StartupOptions.FFmpegPath, + StartupOptions.FFprobePath, + ServerConfigurationManager, + FileSystemManager, + () => SubtitleEncoder, + () => MediaSourceManager, + ProcessFactory, + 5000); serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); @@ -908,27 +918,6 @@ namespace Emby.Server.Implementations return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); } - /// - /// Registers the media encoder. - /// - /// Task. - private void RegisterMediaEncoder(IAssemblyInfo assemblyInfo) - { - MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory, - JsonSerializer, - StartupOptions.FFmpegPath, - StartupOptions.FFprobePath, - ServerConfigurationManager, - FileSystemManager, - () => SubtitleEncoder, - () => MediaSourceManager, - ProcessFactory, - 5000); - - RegisterSingleInstance(MediaEncoder); - } - /// /// Gets the user repository. /// @@ -1404,7 +1393,7 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = localAddress, SupportsLibraryMonitor = true, - EncoderLocationType = MediaEncoder.EncoderLocationType, + EncoderLocation = MediaEncoder.EncoderLocation, SystemArchitecture = EnvironmentInfo.SystemArchitecture, SystemUpdateLevel = SystemUpdateLevel, PackageName = StartupOptions.PackageName diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 057e439104..8852dac051 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.System; namespace MediaBrowser.Controller.MediaEncoding { @@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { - string EncoderLocationType { get; } + FFmpegLocation EncoderLocation { get; } /// /// Gets the encoder path. diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 1eeea87a06..3eed891cb3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(output)) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + } return false; } @@ -55,6 +59,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); + } return false; } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 36d72cad90..9aad67ec70 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -18,6 +19,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -34,17 +36,16 @@ namespace MediaBrowser.MediaEncoding.Encoder public string EncoderPath => FFmpegPath; /// - /// External: path supplied via command line - /// Custom: coming from UI or config/encoding.xml file - /// System: FFmpeg found in system $PATH - /// null: No FFmpeg found + /// The location of the discovered FFmpeg tool. /// - public string EncoderLocationType { get; private set; } + public FFmpegLocation EncoderLocation { get; private set; } + + private FFmpegLocation ProbeLocation; private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; - private string FFmpegPath { get; set; } - private string FFprobePath { get; set; } + private string FFmpegPath; + private string FFprobePath; protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; protected readonly Func SubtitleEncoder; @@ -54,6 +55,11 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string StartupOptionFFmpegPath; private readonly string StartupOptionFFprobePath; + /// + /// Enum to identify the two types of FF utilities of interest. + /// + private enum FFtype { Mpeg, Probe }; + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly List _runningProcesses = new List(); @@ -82,48 +88,24 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Run at startup or if the user removes a Custom path from transcode page. - /// Sets global variables FFmpegPath and EncoderLocationType. - /// If startup options --ffprobe is given then FFprobePath is set too. + /// Sets global variables FFmpegPath. + /// Precedence is: Config > CLI > $PATH /// public void Init() { - // 1) If given, use the --ffmpeg CLI switch - if (ValidatePathFFmpeg("From CLI Switch", StartupOptionFFmpegPath)) + // 1) Custom path stored in config/encoding xml file under tag takes precedence + if (!ValidatePath(FFtype.Mpeg, ConfigurationManager.GetConfiguration("encoding").EncoderAppPathCustom, FFmpegLocation.Custom)) { - _logger.LogInformation("FFmpeg: Using path from command line switch --ffmpeg"); - EncoderLocationType = "External"; - } - - // 2) Try Custom path stroed in config/encoding xml file under tag - else if (ValidatePathFFmpeg("From Config File", ConfigurationManager.GetConfiguration("encoding").EncoderAppPathCustom)) - { - _logger.LogInformation("FFmpeg: Using path from config/encoding.xml file"); - EncoderLocationType = "Custom"; - } - - // 3) Search system $PATH environment variable for valid FFmpeg - else if (ValidatePathFFmpeg("From $PATH", ExistsOnSystemPath("ffmpeg"))) - { - _logger.LogInformation("FFmpeg: Using system $PATH for FFmpeg"); - EncoderLocationType = "System"; - } - else - { - _logger.LogError("FFmpeg: No suitable executable found"); - FFmpegPath = null; - EncoderLocationType = null; - } - - // If given, use the --ffprobe CLI switch - if (ValidatePathFFprobe("CLI Switch", StartupOptionFFprobePath)) - { - _logger.LogInformation("FFprobe: Using path from command line switch --ffprobe"); - } - else - { - // FFprobe path from command line is no good, so set to null and let ReInit() try - // and set using the FFmpeg path. - FFprobePath = null; + // 2) Check if the --ffmpeg CLI switch has been given + if (!ValidatePath(FFtype.Mpeg, StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + { + // 3) Search system $PATH environment variable for valid FFmpeg + if (!ValidatePath(FFtype.Mpeg, ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) + { + EncoderLocation = FFmpegLocation.NotFound; + FFmpegPath = null; + } + } } ReInit(); @@ -136,27 +118,27 @@ namespace MediaBrowser.MediaEncoding.Encoder /// private void ReInit() { - // Write the FFmpeg path to the config/encoding.xml file so it appears in UI + // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI var config = ConfigurationManager.GetConfiguration("encoding"); config.EncoderAppPath = FFmpegPath ?? string.Empty; ConfigurationManager.SaveConfiguration("encoding", config); + // Clear probe settings in case probe validation fails + ProbeLocation = FFmpegLocation.NotFound; + FFprobePath = null; + // Only if mpeg path is set, try and set path to probe if (FFmpegPath != null) { - // Probe would be null here if no valid --ffprobe path was given - // at startup, or we're performing ReInit following mpeg path update from UI - if (FFprobePath == null) + if (EncoderLocation == FFmpegLocation.Custom || StartupOptionFFprobePath == null) { - // Use the mpeg path to create a probe path - if (ValidatePathFFprobe("Copied from FFmpeg:", GetProbePathFromEncoderPath(FFmpegPath))) - { - _logger.LogInformation("FFprobe: Using FFprobe in same folders as FFmpeg"); - } - else - { - _logger.LogError("FFprobe: No suitable executable found"); - } + // If mpeg was read from config, or CLI switch not given, try and set probe from mpeg path + ValidatePath(FFtype.Probe, GetProbePathFromEncoderPath(FFmpegPath), EncoderLocation); + } + else + { + // Else try and set probe path from CLI switch + ValidatePath(FFtype.Probe, StartupOptionFFmpegPath, FFmpegLocation.SetByArgument); } // Interrogate to understand what coders it supports @@ -183,108 +165,95 @@ namespace MediaBrowser.MediaEncoding.Encoder { throw new ArgumentException("Unexpected pathType value"); } + + if (string.IsNullOrWhiteSpace(path)) + { + // User had cleared the custom path in UI. Clear the Custom config + // setting and perform full Init to reinspect any CLI switches and system $PATH + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathCustom = string.Empty; + ConfigurationManager.SaveConfiguration("encoding", config); + + Init(); + } + else if (!File.Exists(path) && !Directory.Exists(path)) + { + // Given path is neither file or folder + throw new ResourceNotFoundException(); + } else { - if (string.IsNullOrWhiteSpace(path)) + // Supplied path could be either file path or folder path. + // Resolve down to file path and validate + if (!ValidatePath(FFtype.Mpeg, GetEncoderPath(path), FFmpegLocation.Custom)) { - // User had cleared the cutom path in UI. Clear the Custom config - // setting and peform full Init to relook any CLI switches and system $PATH - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathCustom = string.Empty; - ConfigurationManager.SaveConfiguration("encoding", config); - - Init(); - } - else if (!File.Exists(path) && !Directory.Exists(path)) - { - // Given path is neither file or folder - throw new ResourceNotFoundException(); + throw new ResourceNotFoundException("Failed validation checks."); } else { - // Supplied path could be either file path or folder path. - // Resolve down to file path and validate - path = GetEncoderPath(path); + // Write the validated mpeg path to the xml as + // This ensures its not lost on new startup + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathCustom = FFmpegPath; + ConfigurationManager.SaveConfiguration("encoding", config); - if (path == null) - { - throw new ResourceNotFoundException("FFmpeg not found"); - } - else if (!ValidatePathFFmpeg("New From UI", path)) - { - throw new ResourceNotFoundException("Failed validation checks. Version 4.0 or greater is required"); - } - else - { - EncoderLocationType = "Custom"; - - // Write the validated mpeg path to the xml as - // This ensures its not lost on new startup - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathCustom = FFmpegPath; - ConfigurationManager.SaveConfiguration("encoding", config); - - FFprobePath = null; // Clear probe path so it gets relooked in ReInit() - - ReInit(); - } + ReInit(); } } } - private bool ValidatePath(string type, string path) + /// + /// Validates the supplied FQPN to ensure it is a FFxxx utility. + /// If checks pass, global variable FFmpegPath (or FFprobePath) and + /// EncoderLocation (or ProbeLocation) are updated. + /// + /// Either mpeg or probe + /// FQPN to test + /// Location (External, Custom, System) of tool + /// + private bool ValidatePath(FFtype type, string path, FFmpegLocation location) { + bool rc = false; + if (!string.IsNullOrEmpty(path)) { if (File.Exists(path)) { - var valid = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); + rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, false); - if (valid == true) + // Only update the global variables if the checks passed + if (rc) { - return true; + if (type == FFtype.Mpeg) + { + FFmpegPath = path; + EncoderLocation = location; + } + else + { + FFprobePath = path; + ProbeLocation = location; + } } else { - _logger.LogError("{0}: Failed validation checks. Version 4.0 or greater is required: {1}", type, path); + _logger.LogError("{0}: {1}: Failed version check: {2}", type.ToString(), location.ToString(), path); } } else { - _logger.LogError("{0}: File not found: {1}", type, path); + _logger.LogError("{0}: {1}: File not found: {2}", type.ToString(), location.ToString(), path); } } - return false; - } - - private bool ValidatePathFFmpeg(string comment, string path) - { - if (ValidatePath("FFmpeg: " + comment, path) == true) - { - FFmpegPath = path; - return true; - } - - return false; - } - - private bool ValidatePathFFprobe(string comment, string path) - { - if (ValidatePath("FFprobe: " + comment, path) == true) - { - FFprobePath = path; - return true; - } - - return false; + return rc; } private string GetEncoderPath(string path) { if (Directory.Exists(path)) { - return GetEncoderPathFromDirectory(path); + return GetEncoderPathFromDirectory(path, "ffmpeg"); } if (File.Exists(path)) @@ -295,7 +264,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return null; } - private string GetEncoderPathFromDirectory(string path) + private string GetEncoderPathFromDirectory(string path, string filename) { try { @@ -303,7 +272,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var excludeExtensions = new[] { ".c" }; - return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) + && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); } catch (Exception) { @@ -314,8 +284,15 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProbePathFromEncoderPath(string appPath) { - return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath)) - .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrEmpty(appPath)) + { + string pattern = @"[^\/\\]+?(\.[^\/\\\n.]+)?$"; + string substitution = @"ffprobe$1"; + + return Regex.Replace(appPath, pattern, substitution); + } + + return null; } /// @@ -323,15 +300,15 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// /// - private string ExistsOnSystemPath(string fileName) + private string ExistsOnSystemPath(string filename) { var values = Environment.GetEnvironmentVariable("PATH"); foreach (var path in values.Split(Path.PathSeparator)) { - var candidatePath = GetEncoderPathFromDirectory(path); + var candidatePath = GetEncoderPathFromDirectory(path, filename); - if (ValidatePath("Found on PATH", candidatePath)) + if (!string.IsNullOrEmpty(candidatePath)) { return candidatePath; } @@ -341,8 +318,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private void LogPaths() { - _logger.LogInformation("FFMpeg: {0}", FFmpegPath ?? "not found"); - _logger.LogInformation("FFProbe: {0}", FFprobePath ?? "not found"); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); + _logger.LogInformation("FFprobe: {0}: {1}", ProbeLocation.ToString(), FFprobePath ?? string.Empty); } private List _encoders = new List(); diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 581a1069cd..6482f2c840 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates; namespace MediaBrowser.Model.System { + /// + /// Enum describing the location of the FFmpeg tool. + /// + public enum FFmpegLocation + { + /// No path to FFmpeg found. + NotFound, + /// Path supplied via command line using switch --ffmpeg. + SetByArgument, + /// User has supplied path via Transcoding UI page. + Custom, + /// FFmpeg tool found on system $PATH. + System + }; + /// /// Class SystemInfo /// @@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System /// true if this instance has update available; otherwise, false. public bool HasUpdateAvailable { get; set; } - public string EncoderLocationType { get; set; } + public FFmpegLocation EncoderLocation { get; set; } public Architecture SystemArchitecture { get; set; } From 632968dc817a398bff84ca3ef82337b4e5786416 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Wed, 13 Feb 2019 16:46:03 +0000 Subject: [PATCH 0153/6208] Added self to contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b397b3280..81857e57c7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -23,6 +23,7 @@ - [fruhnow](https://github.com/fruhnow) - [Lynxy](https://github.com/Lynxy) - [fasheng](https://github.com/fasheng) + - [ploughpuff](https://github.com/ploughpuff) # Emby Contributors From 8104e739d5b83d0d6563bb685f910697e60d6c12 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Wed, 27 Feb 2019 16:33:08 +0000 Subject: [PATCH 0154/6208] Address review comments from Bond --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 ++++++++-- MediaBrowser.Model/Configuration/EncodingOptions.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9aad67ec70..265c043b98 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -282,12 +282,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// + /// With the given path string, replaces the filename with ffprobe, taking case + /// of any file extension (like .exe on windows). + /// + /// + /// private string GetProbePathFromEncoderPath(string appPath) { if (!string.IsNullOrEmpty(appPath)) { - string pattern = @"[^\/\\]+?(\.[^\/\\\n.]+)?$"; - string substitution = @"ffprobe$1"; + const string pattern = @"[^\/\\]+?(\.[^\/\\\n.]+)?$"; + const string substitution = @"ffprobe$1"; return Regex.Replace(appPath, pattern, substitution); } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index ff697437ac..7fc985ba02 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -8,8 +8,14 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } - public string EncoderAppPathCustom { get; set; } // FFmpeg path as set by the user via the UI - public string EncoderAppPath { get; set; } // The current FFmpeg path being used by the system + /// + /// FFmpeg path as set by the user via the UI + /// + public string EncoderAppPathCustom { get; set; } + /// + /// The current FFmpeg path being used by the system + /// + public string EncoderAppPath { get; set; } public string VaapiDevice { get; set; } public int H264Crf { get; set; } public string H264Preset { get; set; } From 656bffbbb291dc9b58443bb36c8ed7d3688f6c1b Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Thu, 28 Feb 2019 22:06:56 +0000 Subject: [PATCH 0155/6208] Remove --ffprobe logic --- Jellyfin.Server/StartupOptions.cs | 4 +- .../Encoder/MediaEncoder.cs | 185 +++++------------- .../Configuration/EncodingOptions.cs | 8 +- 3 files changed, 59 insertions(+), 138 deletions(-) diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 5d3f7b171b..c8cdb984db 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -20,10 +20,10 @@ namespace Jellyfin.Server [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] public string LogDir { get; set; } - [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")] + [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } - [Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")] + [Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")] public string FFprobePath { get; set; } [Option("service", Required = false, HelpText = "Run as headless service.")] diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 265c043b98..51b4f6e393 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -40,8 +40,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public FFmpegLocation EncoderLocation { get; private set; } - private FFmpegLocation ProbeLocation; - private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private string FFmpegPath; @@ -55,11 +53,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string StartupOptionFFmpegPath; private readonly string StartupOptionFFprobePath; - /// - /// Enum to identify the two types of FF utilities of interest. - /// - private enum FFtype { Mpeg, Probe }; - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly List _runningProcesses = new List(); @@ -93,14 +86,20 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public void Init() { - // 1) Custom path stored in config/encoding xml file under tag takes precedence - if (!ValidatePath(FFtype.Mpeg, ConfigurationManager.GetConfiguration("encoding").EncoderAppPathCustom, FFmpegLocation.Custom)) + // ToDo - Finalise removal of the --ffprobe switch + if (!string.IsNullOrEmpty(StartupOptionFFprobePath)) + { + _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release"); + } + + // 1) Custom path stored in config/encoding xml file under tag takes precedence + if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(FFtype.Mpeg, StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) { // 3) Search system $PATH environment variable for valid FFmpeg - if (!ValidatePath(FFtype.Mpeg, ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) + if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) { EncoderLocation = FFmpegLocation.NotFound; FFmpegPath = null; @@ -108,110 +107,80 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - ReInit(); - } - - /// - /// Writes the currently used FFmpeg to config/encoding.xml file. - /// Sets the FFprobe path if not currently set. - /// Interrogates the FFmpeg tool to identify what encoders/decodres are available. - /// - private void ReInit() - { - // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI + // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPath = FFmpegPath ?? string.Empty; + config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; ConfigurationManager.SaveConfiguration("encoding", config); - // Clear probe settings in case probe validation fails - ProbeLocation = FFmpegLocation.NotFound; - FFprobePath = null; - // Only if mpeg path is set, try and set path to probe if (FFmpegPath != null) { - if (EncoderLocation == FFmpegLocation.Custom || StartupOptionFFprobePath == null) - { - // If mpeg was read from config, or CLI switch not given, try and set probe from mpeg path - ValidatePath(FFtype.Probe, GetProbePathFromEncoderPath(FFmpegPath), EncoderLocation); - } - else - { - // Else try and set probe path from CLI switch - ValidatePath(FFtype.Probe, StartupOptionFFmpegPath, FFmpegLocation.SetByArgument); - } + // Determine a probe path from the mpeg path + FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); - // Interrogate to understand what coders it supports + // Interrogate to understand what coders are supported var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath); SetAvailableDecoders(result.decoders); SetAvailableEncoders(result.encoders); } - // Stamp FFmpeg paths to the log file - LogPaths(); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); } /// - /// Triggered from the Settings > Trascoding UI page when users sumits Custom FFmpeg path to use. + /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use. + /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here. /// /// /// public void UpdateEncoderPath(string path, string pathType) { + string newPath; + _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Unexpected pathType value"); } - - if (string.IsNullOrWhiteSpace(path)) + else if (string.IsNullOrWhiteSpace(path)) { - // User had cleared the custom path in UI. Clear the Custom config - // setting and perform full Init to reinspect any CLI switches and system $PATH - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathCustom = string.Empty; - ConfigurationManager.SaveConfiguration("encoding", config); - - Init(); + // User had cleared the custom path in UI + newPath = string.Empty; } - else if (!File.Exists(path) && !Directory.Exists(path)) + else if (File.Exists(path)) { - // Given path is neither file or folder - throw new ResourceNotFoundException(); + newPath = path; + } + else if (Directory.Exists(path)) + { + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); } else { - // Supplied path could be either file path or folder path. - // Resolve down to file path and validate - if (!ValidatePath(FFtype.Mpeg, GetEncoderPath(path), FFmpegLocation.Custom)) - { - throw new ResourceNotFoundException("Failed validation checks."); - } - else - { - // Write the validated mpeg path to the xml as - // This ensures its not lost on new startup - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathCustom = FFmpegPath; - ConfigurationManager.SaveConfiguration("encoding", config); - - ReInit(); - } + throw new ResourceNotFoundException(); } + + // Write the new ffmpeg path to the xml as + // This ensures its not lost on next startup + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPath = newPath; + ConfigurationManager.SaveConfiguration("encoding", config); + + // Trigger Init so we validate the new path and setup probe path + Init(); } /// - /// Validates the supplied FQPN to ensure it is a FFxxx utility. - /// If checks pass, global variable FFmpegPath (or FFprobePath) and - /// EncoderLocation (or ProbeLocation) are updated. + /// Validates the supplied FQPN to ensure it is a ffmpeg utility. + /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. /// - /// Either mpeg or probe /// FQPN to test /// Location (External, Custom, System) of tool /// - private bool ValidatePath(FFtype type, string path, FFmpegLocation location) + private bool ValidatePath(string path, FFmpegLocation location) { bool rc = false; @@ -219,51 +188,28 @@ namespace MediaBrowser.MediaEncoding.Encoder { if (File.Exists(path)) { - rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, false); + rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); - // Only update the global variables if the checks passed - if (rc) + if (!rc) { - if (type == FFtype.Mpeg) - { - FFmpegPath = path; - EncoderLocation = location; - } - else - { - FFprobePath = path; - ProbeLocation = location; - } - } - else - { - _logger.LogError("{0}: {1}: Failed version check: {2}", type.ToString(), location.ToString(), path); + _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path); } + + // ToDo - Enable the ffmpeg validator. At the moment any version can be used. + rc = true; + + FFmpegPath = path; + EncoderLocation = location; } else { - _logger.LogError("{0}: {1}: File not found: {2}", type.ToString(), location.ToString(), path); + _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path); } } return rc; } - private string GetEncoderPath(string path) - { - if (Directory.Exists(path)) - { - return GetEncoderPathFromDirectory(path, "ffmpeg"); - } - - if (File.Exists(path)) - { - return path; - } - - return null; - } - private string GetEncoderPathFromDirectory(string path, string filename) { try @@ -282,25 +228,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - /// - /// With the given path string, replaces the filename with ffprobe, taking case - /// of any file extension (like .exe on windows). - /// - /// - /// - private string GetProbePathFromEncoderPath(string appPath) - { - if (!string.IsNullOrEmpty(appPath)) - { - const string pattern = @"[^\/\\]+?(\.[^\/\\\n.]+)?$"; - const string substitution = @"ffprobe$1"; - - return Regex.Replace(appPath, pattern, substitution); - } - - return null; - } - /// /// Search the system $PATH environment variable looking for given filename. /// @@ -322,12 +249,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return null; } - private void LogPaths() - { - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); - _logger.LogInformation("FFprobe: {0}: {1}", ProbeLocation.ToString(), FFprobePath ?? string.Empty); - } - private List _encoders = new List(); public void SetAvailableEncoders(IEnumerable list) { diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 7fc985ba02..285ff4ba58 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -11,11 +11,11 @@ namespace MediaBrowser.Model.Configuration /// /// FFmpeg path as set by the user via the UI /// - public string EncoderAppPathCustom { get; set; } - /// - /// The current FFmpeg path being used by the system - /// public string EncoderAppPath { get; set; } + /// + /// The current FFmpeg path being used by the system and displayed on the transcode page + /// + public string EncoderAppPathDisplay { get; set; } public string VaapiDevice { get; set; } public int H264Crf { get; set; } public string H264Preset { get; set; } From 2617a49b78c99f72ba36e53a4c97c4e042116a53 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Thu, 28 Feb 2019 22:47:56 +0000 Subject: [PATCH 0156/6208] Renamed Init() to SetFFmpegPath() --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dd29f2adec..325df32931 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -534,7 +534,7 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - MediaEncoder.Init(); + MediaEncoder.SetFFmpegPath(); //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) //{ diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 8852dac051..d4ac3b7c37 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.String. string EscapeSubtitleFilterPath(string path); - void Init(); + void SetFFmpegPath(); void UpdateEncoderPath(string path, string pathType); bool SupportsEncoder(string encoder); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 51b4f6e393..2924577888 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// Sets global variables FFmpegPath. /// Precedence is: Config > CLI > $PATH /// - public void Init() + public void SetFFmpegPath() { // ToDo - Finalise removal of the --ffprobe switch if (!string.IsNullOrEmpty(StartupOptionFFprobePath)) @@ -169,8 +169,8 @@ namespace MediaBrowser.MediaEncoding.Encoder config.EncoderAppPath = newPath; ConfigurationManager.SaveConfiguration("encoding", config); - // Trigger Init so we validate the new path and setup probe path - Init(); + // Trigger SetFFmpegPath so we validate the new path and setup probe path + SetFFmpegPath(); } /// From 9f6bca36586646835655de90505b306cbcaa92aa Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 5 Mar 2019 23:22:07 -0500 Subject: [PATCH 0157/6208] Put output into bin instead of jellyfin-build --- build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build b/build index 3b4167dae4..c3deaba329 100755 --- a/build +++ b/build @@ -217,7 +217,7 @@ for target_platform in ${platform[@]}; do done if [[ -d pkg-dist/ ]]; then echo -e ">> Collecting build artifacts" - target_dir="../../../jellyfin-build/${target_platform}" + target_dir="../../../bin/${target_platform}" mkdir -p ${target_dir} mv pkg-dist/* ${target_dir}/ fi From a994edda044f71ddee360e1120345d8f0ddea81e Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 5 Mar 2019 23:27:17 -0500 Subject: [PATCH 0158/6208] Allow complete ignoring of submodule Used by the parent build infrastructure --- build | 58 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/build b/build index c3deaba329..51d4b79a20 100755 --- a/build +++ b/build @@ -26,7 +26,7 @@ usage() { echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch ] " echo -e "" echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds." - echo -e "The web_branch defaults to the same branch name as the current main branch." + echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching." echo -e "To build all platforms, use 'all'." echo -e "To perform all build actions, use 'all'." echo -e "Build output files are collected at '../jellyfin-build/'." @@ -164,38 +164,40 @@ for target_platform in ${platform[@]}; do fi done -# Initialize submodules -git submodule update --init --recursive +if [[ ${web_branch} != 'local' ]]; then + # Initialize submodules + git submodule update --init --recursive -# configure branch -pushd MediaBrowser.WebDashboard/jellyfin-web + # configure branch + pushd MediaBrowser.WebDashboard/jellyfin-web -if ! git diff-index --quiet HEAD --; then + if ! git diff-index --quiet HEAD --; then + popd + echo + echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!" + echo "This script will overwrite your unstaged and unpushed changes." + echo "Please do development on 'jellyfin-web' outside of the submodule." + exit 1 + fi + + git fetch --all + # If this is an official branch name, fetch it from origin + official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$" + if [[ ${web_branch} =~ ${official_branches_regex} ]]; then + git checkout origin/${web_branch} || { + echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid." + exit 1 + } + # Otherwise, just check out the local branch (for testing, etc.) + else + git checkout ${web_branch} || { + echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid." + exit 1 + } + fi popd - echo - echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!" - echo "This script will overwrite your unstaged and unpushed changes." - echo "Please do development on 'jellyfin-web' outside of the submodule." - exit 1 fi -git fetch --all -# If this is an official branch name, fetch it from origin -official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$" -if [[ ${web_branch} =~ ${official_branches_regex} ]]; then - git checkout origin/${web_branch} || { - echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid." - exit 1 - } -# Otherwise, just check out the local branch (for testing, etc.) -else - git checkout ${web_branch} || { - echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid." - exit 1 - } -fi -popd - # Execute each platform and action in order, if said action is enabled pushd deployment/ for target_platform in ${platform[@]}; do From bf9f342c1321e1644246ba9a92cb76518e9180e4 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 5 Mar 2019 23:53:07 -0500 Subject: [PATCH 0159/6208] Add build.yaml for parent build infrastructure --- build.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 build.yaml diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000000..b0d2502d53 --- /dev/null +++ b/build.yaml @@ -0,0 +1,15 @@ +--- +# We just wrap `build` so this is really it +name: "jellyfin" +version: "10.2.2" +packages: + - debian-package-x64 + - debian-package-armhf + - ubuntu-package-x64 + - fedora-package-x64 + - centos-package-x64 + - linux-x64 + - macos + - portable + - win-x64 + - win-x86 From bef665be364ce1477d09ed268f68c19e0099922f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 5 Mar 2019 23:45:05 -0800 Subject: [PATCH 0160/6208] Minor fixes to address style issues --- .../Library/DefaultAuthenticationProvider.cs | 14 +++++++------- .../Library/UserManager.cs | 15 +++++---------- MediaBrowser.Model/Cryptography/PasswordHash.cs | 7 ++++--- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 8f10b5a84e..3ac604b40e 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -50,22 +50,22 @@ namespace Emby.Server.Implementations.Library byte[] passwordbytes = Encoding.UTF8.GetBytes(password); PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] CalculatedHash; - string CalculatedHashString; + byte[] calculatedHash; + string calculatedHashString; if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) { if (string.IsNullOrEmpty(readyHash.Salt)) { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); } else { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); } - if (CalculatedHashString == readyHash.Hash) + if (calculatedHashString == readyHash.Hash) { success = true; // throw new Exception("Invalid username or password"); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 57bf16364d..efb1ef4a50 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -475,11 +475,6 @@ namespace Emby.Server.Implementations.Library : user.EasyPassword; } - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.IsNullOrEmpty(passwordHash); - } - /// /// Loads the users from the repository /// @@ -522,14 +517,14 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); - var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - var dto = new UserDto + UserDto dto = new UserDto { Id = user.Id, Name = user.Name, @@ -548,7 +543,7 @@ namespace Emby.Server.Implementations.Library dto.EnableAutoLogin = true; } - var image = user.GetImageInfo(ImageType.Primary, 0); + ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); if (image != null) { diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index a528404045..7a1be833db 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -100,13 +100,14 @@ namespace MediaBrowser.Model.Cryptography public static byte[] ConvertFromByteString(string byteString) { - List Bytes = new List(); + List bytes = new List(); for (int i = 0; i < byteString.Length; i += 2) { - Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); + // TODO: NetStandard2.1 switch this to use a span instead of a substring. + bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } - return Bytes.ToArray(); + return bytes.ToArray(); } public static string ConvertToByteString(byte[] bytes) From 4ef7eda593ba9061db5b44e1d11939421550783b Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 6 Mar 2019 09:22:38 -0500 Subject: [PATCH 0161/6208] Copy install script from new location --- deployment/win-x64/package.sh | 4 ++-- deployment/win-x86/package.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh index d21e3b5325..b438c28e4b 100755 --- a/deployment/win-x64/package.sh +++ b/deployment/win-x64/package.sh @@ -21,8 +21,8 @@ package_win64() ( cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe rm -r ${TEMP_DIR} - cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 - cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat + cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 + cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat mkdir -p ${PKG_DIR} pushd ${OUTPUT_DIR} ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip . diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh index 3cc4eb6239..8752d92a89 100755 --- a/deployment/win-x86/package.sh +++ b/deployment/win-x86/package.sh @@ -20,8 +20,8 @@ package_win32() ( cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe rm -r ${TEMP_DIR} - cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 - cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat + cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 + cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat mkdir -p ${PKG_DIR} pushd ${OUTPUT_DIR} ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip . From 04db0369d46c4b3961086b1d30946bd990a5767c Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 6 Mar 2019 17:31:52 +0100 Subject: [PATCH 0162/6208] Update LocalizationManager.cs --- .../Localization/LocalizationManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index d55b258a3d..762649b716 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -93,12 +93,11 @@ namespace Emby.Server.Implementations.Localization #if DEBUG else { - _logger.LogWarning("Misformed line in ratings file for country {CountryCode}", countryCode); + _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); } #endif } } - _logger.LogWarning("{t}", countryCode); _allParentalRatings[countryCode] = dict; } From 394d23a73a530b5ce2609d01575e0ced734aecfe Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 6 Mar 2019 19:14:03 +0100 Subject: [PATCH 0163/6208] Review comments --- Emby.Server.Implementations/WebSockets/WebSocketManager.cs | 2 +- .../Playback/Progressive/BaseProgressiveStreamingService.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs index 5db2c8da73..04c73ecea7 100644 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.WebSockets ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length) : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length); - // All messages are expected to be json + // All messages are expected to be valid JSON objects if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message); diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index fc81e532d8..399ac2be09 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -362,6 +362,7 @@ namespace MediaBrowser.Api.Playback.Progressive var contentType = state.GetMimeType(outputPath); + // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response // Headers only if (isHeadRequest) { From 21c2acc520bcebe74dacc4caf1c244e258a12aec Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 6 Mar 2019 19:27:05 +0100 Subject: [PATCH 0164/6208] Remove public Host property --- Emby.Server.Implementations/ApplicationHost.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 93147539c6..71a112dacf 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -129,7 +129,7 @@ namespace Emby.Server.Implementations /// /// true if this instance can self restart; otherwise, false. public abstract bool CanSelfRestart { get; } - public IWebHost Host { get; set; } + public virtual bool CanLaunchWebBrowser { get @@ -625,7 +625,7 @@ namespace Emby.Server.Implementations contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src"); } - Host = new WebHostBuilder() + var host = new WebHostBuilder() .UseKestrel(options => { options.ListenAnyIP(HttpPort); @@ -652,10 +652,10 @@ namespace Emby.Server.Implementations }) .Build(); - await Host.StartAsync(); + await host.StartAsync(); } - public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) + private async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) { if (!context.WebSockets.IsWebSocketRequest) { @@ -665,7 +665,8 @@ namespace Emby.Server.Implementations await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); } - public async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) + + private async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) { if (context.WebSockets.IsWebSocketRequest) { From bba049c987a632375da5e847512efe7ec4a85a31 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 6 Mar 2019 19:29:25 +0100 Subject: [PATCH 0165/6208] Make FileSystem readonly --- Emby.Server.Implementations/HttpServer/FileWriter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 78e2015b1e..0e989994b1 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.HttpServer { private readonly IStreamHelper _streamHelper; private ILogger Logger { get; set; } - public IFileSystem FileSystem { get; } + private readonly IFileSystem _fileSystem; private string RangeHeader { get; set; } private bool IsHeadRequest { get; set; } @@ -54,10 +54,10 @@ namespace Emby.Server.Implementations.HttpServer } _streamHelper = streamHelper; + _fileSystem = fileSystem; Path = path; Logger = logger; - FileSystem = fileSystem; RangeHeader = rangeHeader; Headers[HeaderNames.ContentType] = contentType; @@ -181,7 +181,7 @@ namespace Emby.Server.Implementations.HttpServer count = 0; } - await response.TransmitFile(path, offset, count, FileShare, FileSystem, _streamHelper, cancellationToken).ConfigureAwait(false); + await response.TransmitFile(path, offset, count, FileShare, _fileSystem, _streamHelper, cancellationToken).ConfigureAwait(false); } finally { From c31b0b311b339475650aa8812eb57152cac32d80 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 02:41:44 -0800 Subject: [PATCH 0166/6208] Apply suggestions from code review more minor fixes before I do larger fixes Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Cryptography/CryptographyProvider.cs | 6 +- .../Library/DefaultAuthenticationProvider.cs | 2 +- .../Cryptography/PasswordHash.cs | 132 +++++++++--------- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index cf1ea6efa3..2e882cefed 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.Cryptography { //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) + if (method == DefaultHashMethod) { using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) { @@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Cryptography public byte[] ComputeHash(string hashMethod, byte[] bytes) { - return ComputeHash(hashMethod, bytes, new byte[0]); + return ComputeHash(hashMethod, bytes, Array.Empty()); } public byte[] ComputeHashWithDefaultMethod(byte[] bytes) @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.Cryptography public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { - if(hashMethod == DefaultHashMethod) + if (hashMethod == DefaultHashMethod) { return PBKDF2(hashMethod, bytes, salt, _defaultIterations); } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 3ac604b40e..526509f439 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library if (!user.Password.Contains("$")) { string hash = user.Password; - user.Password = String.Format("$SHA1${0}", hash); + user.Password = string.Format("$SHA1${0}", hash); } if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 7a1be833db..72bdc6745e 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -8,34 +8,34 @@ namespace MediaBrowser.Model.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 + // $[$=(,=)*][$[$]] + // 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 string _id; - private Dictionary _parameters = new Dictionary(); + private Dictionary _parameters = new Dictionary(); - private string _salt; + private string _salt; - private byte[] _saltBytes; + private byte[] _saltBytes; - private string _hash; + private string _hash; + + private byte[] _hashBytes; + + public string Id { get => _id; set => _id = value; } + + public Dictionary Parameters { get => _parameters; set => _parameters = value; } + + public string Salt { get => _salt; set => _salt = value; } + + public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } + + public string Hash { get => _hash; set => _hash = value; } + + public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } - private byte[] _hashBytes; - - public string Id { get => _id; set => _id = value; } - - public Dictionary Parameters { get => _parameters; set => _parameters = value; } - - public string Salt { get => _salt; set => _salt = value; } - - public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } - - public string Hash { get => _hash; set => _hash = value; } - - public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } - public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); @@ -46,14 +46,14 @@ namespace MediaBrowser.Model.Cryptography { if (!string.IsNullOrEmpty(paramset)) { - string[] fields = paramset.Split('='); - if (fields.Length == 2) - { - _parameters.Add(fields[0], fields[1]); - } - else - { - throw new Exception($"Malformed parameter in password hash string {paramset}"); + string[] fields = paramset.Split('='); + if (fields.Length == 2) + { + _parameters.Add(fields[0], fields[1]); + } + else + { + throw new Exception($"Malformed parameter in password hash string {paramset}"); } } } @@ -89,31 +89,31 @@ namespace MediaBrowser.Model.Cryptography } - } - + } + public PasswordHash(ICryptoProvider cryptoProvider) { _id = cryptoProvider.DefaultHashMethod; _saltBytes = cryptoProvider.GenerateSalt(); - _salt = ConvertToByteString(SaltBytes); - } - - public static byte[] ConvertFromByteString(string byteString) - { - List bytes = new List(); - for (int i = 0; i < byteString.Length; i += 2) - { - // TODO: NetStandard2.1 switch this to use a span instead of a substring. - bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); - } - - return bytes.ToArray(); - } - - public static string ConvertToByteString(byte[] bytes) - { - return BitConverter.ToString(bytes).Replace("-", ""); - } + _salt = ConvertToByteString(SaltBytes); + } + + public static byte[] ConvertFromByteString(string byteString) + { + List bytes = new List(); + for (int i = 0; i < byteString.Length; i += 2) + { + // TODO: NetStandard2.1 switch this to use a span instead of a substring. + bytes.Add(Convert.ToByte(byteString.Substring(i, 2), 16)); + } + + return bytes.ToArray(); + } + + public static string ConvertToByteString(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", ""); + } private string SerializeParameters() { @@ -121,33 +121,33 @@ namespace MediaBrowser.Model.Cryptography foreach (var KVP in _parameters) { ReturnString += $",{KVP.Key}={KVP.Value}"; - } + } if ((!string.IsNullOrEmpty(ReturnString)) && ReturnString[0] == ',') { ReturnString = ReturnString.Remove(0, 1); - } + } return ReturnString; } public override string ToString() - { - string outString = "$" +_id; - string paramstring = SerializeParameters(); - if (!string.IsNullOrEmpty(paramstring)) - { - outString += $"${paramstring}"; - } - - if (!string.IsNullOrEmpty(_salt)) - { - outString += $"${_salt}"; - } - + { + string outString = "$" + _id; + string paramstring = SerializeParameters(); + if (!string.IsNullOrEmpty(paramstring)) + { + outString += $"${paramstring}"; + } + + if (!string.IsNullOrEmpty(_salt)) + { + outString += $"${_salt}"; + } + outString += $"${_hash}"; return outString; } } -} +} From a9302b8b53e8393fbedeefb3be75ae6eba8ae815 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 1 Feb 2019 17:43:31 +0100 Subject: [PATCH 0167/6208] Remove useless abstraction around XmlReaderSettings This removes the amount of stuff that needs to be passed around Also removes some unneeded `ManagedFileSystem` usage --- .../ConnectionManager/ConnectionManager.cs | 7 +- Emby.Dlna/ConnectionManager/ControlHandler.cs | 4 +- .../ContentDirectory/ContentDirectory.cs | 11 +- Emby.Dlna/ContentDirectory/ControlHandler.cs | 19 +- Emby.Dlna/Main/DlnaEntryPoint.cs | 15 +- .../MediaReceiverRegistrar/ControlHandler.cs | 5 +- .../MediaReceiverRegistrar.cs | 7 +- Emby.Dlna/Service/BaseControlHandler.cs | 17 +- .../ApplicationHost.cs | 4 - .../Xml/XmlReaderSettingsFactory.cs | 20 - .../Parsers/BaseItemXmlParser.cs | 20 +- .../Parsers/BoxSetXmlParser.cs | 5 +- .../Parsers/PlaylistXmlParser.cs | 5 +- .../Providers/BoxSetXmlProvider.cs | 7 +- .../Providers/PlaylistXmlProvider.cs | 7 +- .../Savers/BaseXmlSaver.cs | 7 +- .../Savers/BoxSetXmlSaver.cs | 4 +- .../Savers/PlaylistXmlSaver.cs | 4 +- .../Xml/IXmlReaderSettingsFactory.cs | 9 - .../Music/MusicBrainzAlbumProvider.cs | 175 ++++---- .../Music/MusicBrainzArtistProvider.cs | 19 +- .../TV/MissingEpisodeProvider.cs | 3 - .../TV/SeriesMetadataService.cs | 4 - .../TV/TheTVDB/TvdbPrescanTask.cs | 396 ++++++++++++++++++ .../TV/TheTVDB/TvdbSeriesProvider.cs | 1 - .../Parsers/BaseNfoParser.cs | 34 +- .../Parsers/EpisodeNfoParser.cs | 5 +- .../Parsers/MovieNfoParser.cs | 19 +- .../Parsers/SeasonNfoParser.cs | 5 +- .../Parsers/SeriesNfoParser.cs | 5 +- .../Providers/AlbumNfoProvider.cs | 7 +- .../Providers/ArtistNfoProvider.cs | 7 +- .../Providers/BaseVideoNfoProvider.cs | 7 +- .../Providers/EpisodeNfoProvider.cs | 7 +- .../Providers/MovieNfoProvider.cs | 10 +- .../Providers/SeasonNfoProvider.cs | 11 +- .../Providers/SeriesNfoProvider.cs | 13 +- .../Savers/AlbumNfoSaver.cs | 4 +- .../Savers/ArtistNfoSaver.cs | 4 +- .../Savers/BaseNfoSaver.cs | 87 ++-- .../Savers/EpisodeNfoSaver.cs | 4 +- .../Savers/MovieNfoSaver.cs | 4 +- .../Savers/SeasonNfoSaver.cs | 6 +- .../Savers/SeriesNfoSaver.cs | 4 +- 44 files changed, 654 insertions(+), 364 deletions(-) delete mode 100644 Emby.Server.Implementations/Xml/XmlReaderSettingsFactory.cs delete mode 100644 MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs index cc427f2a15..679eecc891 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -3,7 +3,6 @@ using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.ConnectionManager @@ -13,15 +12,13 @@ namespace Emby.Dlna.ConnectionManager private readonly IDlnaManager _dlna; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient) : base(logger, httpClient) { _dlna = dlna; _config = config; _logger = logger; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } public string GetServiceXml(IDictionary headers) @@ -34,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - return new ControlHandler(_config, _logger, XmlReaderSettingsFactory, profile).ProcessControlRequest(request); + return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request); } } } diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 16211c61f4..2e11047487 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -4,7 +4,6 @@ using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.ConnectionManager @@ -32,7 +31,8 @@ namespace Emby.Dlna.ConnectionManager }; } - public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory, DeviceProfile profile) : base(config, logger, xmlReaderSettingsFactory) + public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) + : base(config, logger) { _profile = profile; } diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs index b0fec90e69..3e2927845e 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.ContentDirectory @@ -28,7 +27,6 @@ namespace Emby.Dlna.ContentDirectory private readonly IMediaSourceManager _mediaSourceManager; private readonly IUserViewManager _userViewManager; private readonly IMediaEncoder _mediaEncoder; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; private readonly ITVSeriesManager _tvSeriesManager; public ContentDirectory(IDlnaManager dlna, @@ -38,7 +36,12 @@ namespace Emby.Dlna.ContentDirectory IServerConfigurationManager config, IUserManager userManager, ILogger logger, - IHttpClient httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) + IHttpClient httpClient, + ILocalizationManager localization, + IMediaSourceManager mediaSourceManager, + IUserViewManager userViewManager, + IMediaEncoder mediaEncoder, + ITVSeriesManager tvSeriesManager) : base(logger, httpClient) { _dlna = dlna; @@ -51,7 +54,6 @@ namespace Emby.Dlna.ContentDirectory _mediaSourceManager = mediaSourceManager; _userViewManager = userViewManager; _mediaEncoder = mediaEncoder; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; _tvSeriesManager = tvSeriesManager; } @@ -94,7 +96,6 @@ namespace Emby.Dlna.ContentDirectory _mediaSourceManager, _userViewManager, _mediaEncoder, - XmlReaderSettingsFactory, _tvSeriesManager) .ProcessControlRequest(request); } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 84f38ff769..4f8c89e485 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -25,7 +25,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.ContentDirectory @@ -51,8 +50,22 @@ namespace Emby.Dlna.ContentDirectory private readonly DeviceProfile _profile; - public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) - : base(config, logger, xmlReaderSettingsFactory) + public ControlHandler( + ILogger logger, + ILibraryManager libraryManager, + DeviceProfile profile, + string serverAddress, + string accessToken, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + User user, int systemUpdateId, + IServerConfigurationManager config, + ILocalizationManager localization, + IMediaSourceManager mediaSourceManager, + IUserViewManager userViewManager, + IMediaEncoder mediaEncoder, + ITVSeriesManager tvSeriesManager) + : base(config, logger) { _libraryManager = libraryManager; _userDataManager = userDataManager; diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 57ed0097a0..1339ba9e4c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; @@ -20,7 +19,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; @@ -48,7 +46,7 @@ namespace Emby.Dlna.Main private readonly IDeviceDiscovery _deviceDiscovery; private SsdpDevicePublisher _Publisher; - + private readonly ISocketFactory _socketFactory; private readonly IEnvironmentInfo _environmentInfo; private readonly INetworkManager _networkManager; @@ -79,7 +77,6 @@ namespace Emby.Dlna.Main IEnvironmentInfo environmentInfo, INetworkManager networkManager, IUserViewManager userViewManager, - IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager) { _config = config; @@ -100,7 +97,8 @@ namespace Emby.Dlna.Main _networkManager = networkManager; _logger = loggerFactory.CreateLogger("Dlna"); - ContentDirectory = new ContentDirectory.ContentDirectory(dlnaManager, + ContentDirectory = new ContentDirectory.ContentDirectory( + dlnaManager, userDataManager, imageProcessor, libraryManager, @@ -112,12 +110,11 @@ namespace Emby.Dlna.Main mediaSourceManager, userViewManager, mediaEncoder, - xmlReaderSettingsFactory, tvSeriesManager); - ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient, xmlReaderSettingsFactory); + ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config, xmlReaderSettingsFactory); + MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config); Current = this; } @@ -260,7 +257,7 @@ namespace Emby.Dlna.Main var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); + _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address.ToString()); var descriptorUri = "/dlna/" + udn + "/description.xml"; var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index ae8175f4a2..7381e52582 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar @@ -36,8 +35,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar }; } - public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) - : base(config, logger, xmlReaderSettingsFactory) + public ControlHandler(IServerConfigurationManager config, ILogger logger) + : base(config, logger) { } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index 2b84528eab..eeae3acb6f 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar @@ -10,13 +9,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar { private readonly IServerConfigurationManager _config; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) : base(logger, httpClient) { _config = config; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } public string GetServiceXml(IDictionary headers) @@ -28,7 +25,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar { return new ControlHandler( _config, - Logger, XmlReaderSettingsFactory) + Logger) .ProcessControlRequest(request); } } diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 5f78674b88..067d5fa43f 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -7,7 +7,6 @@ using System.Xml; using Emby.Dlna.Didl; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service @@ -18,13 +17,11 @@ namespace Emby.Dlna.Service protected readonly IServerConfigurationManager Config; protected readonly ILogger _logger; - protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - protected BaseControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) { Config = config; _logger = logger; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } public ControlResponse ProcessControlRequest(ControlRequest request) @@ -61,11 +58,13 @@ namespace Emby.Dlna.Service using (var streamReader = new StreamReader(request.InputXml)) { - var readerSettings = XmlReaderSettingsFactory.Create(false); - - readerSettings.CheckCharacters = false; - readerSettings.IgnoreProcessingInstructions = true; - readerSettings.IgnoreComments = true; + var readerSettings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; using (var reader = XmlReader.Create(streamReader, readerSettings)) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b3bb4f740e..ec1ad6dbdf 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -44,7 +44,6 @@ using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.Xml; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -98,7 +97,6 @@ using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Updates; -using MediaBrowser.Model.Xml; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Subtitles; @@ -691,8 +689,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(new BdInfoExaminer(FileSystemManager)); - serviceCollection.AddSingleton(new XmlReaderSettingsFactory()); - UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager); serviceCollection.AddSingleton(UserDataManager); diff --git a/Emby.Server.Implementations/Xml/XmlReaderSettingsFactory.cs b/Emby.Server.Implementations/Xml/XmlReaderSettingsFactory.cs deleted file mode 100644 index 308922e6d1..0000000000 --- a/Emby.Server.Implementations/Xml/XmlReaderSettingsFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Xml; -using MediaBrowser.Model.Xml; - -namespace Emby.Server.Implementations.Xml -{ - public class XmlReaderSettingsFactory : IXmlReaderSettingsFactory - { - public XmlReaderSettings Create(bool enableValidation) - { - var settings = new XmlReaderSettings(); - - if (!enableValidation) - { - settings.ValidationType = ValidationType.None; - } - - return settings; - } - } -} diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 38458e34c8..23b9745769 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -10,7 +10,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Parsers @@ -30,19 +29,14 @@ namespace MediaBrowser.LocalMetadata.Parsers private Dictionary _validProviderIds; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - protected IFileSystem FileSystem { get; private set; } - /// /// Initializes a new instance of the class. /// /// The logger. - public BaseItemXmlParser(ILogger logger, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory, IFileSystem fileSystem) + public BaseItemXmlParser(ILogger logger, IProviderManager providerManager) { Logger = logger; ProviderManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; - FileSystem = fileSystem; } /// @@ -64,11 +58,13 @@ namespace MediaBrowser.LocalMetadata.Parsers throw new ArgumentException("The metadata file was empty or null.", nameof(metadataFile)); } - var settings = XmlReaderSettingsFactory.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; _validProviderIds = _validProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index e595e9d068..127334625d 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -3,8 +3,6 @@ using System.Xml; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Parsers @@ -87,7 +85,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Item.LinkedChildren = list.ToArray(); } - public BoxSetXmlParser(ILogger logger, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory, IFileSystem fileSystem) : base(logger, providerManager, xmlReaderSettingsFactory, fileSystem) + public BoxSetXmlParser(ILogger logger, IProviderManager providerManager) + : base(logger, providerManager) { } } diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index ff69cb0237..5608a0be90 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -3,8 +3,6 @@ using System.Xml; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Parsers @@ -95,7 +93,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.LinkedChildren = list.ToArray(); } - public PlaylistXmlParser(ILogger logger, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory, IFileSystem fileSystem) : base(logger, providerManager, xmlReaderSettingsFactory, fileSystem) + public PlaylistXmlParser(ILogger logger, IProviderManager providerManager) + : base(logger, providerManager) { } } diff --git a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs index 539f818c65..2e303efab6 100644 --- a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs @@ -4,7 +4,6 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.LocalMetadata.Parsers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Providers @@ -16,19 +15,17 @@ namespace MediaBrowser.LocalMetadata.Providers { private readonly ILogger _logger; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public BoxSetXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public BoxSetXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) { - new BoxSetXmlParser(_logger, _providerManager, XmlReaderSettingsFactory, FileSystem).Fetch(result, path, cancellationToken); + new BoxSetXmlParser(_logger, _providerManager).Fetch(result, path, cancellationToken); } protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) diff --git a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs index 442a18cb98..d111ae9ba8 100644 --- a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs @@ -4,7 +4,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.LocalMetadata.Parsers; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Providers @@ -13,19 +12,17 @@ namespace MediaBrowser.LocalMetadata.Providers { private readonly ILogger _logger; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public PlaylistXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public PlaylistXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) { - new PlaylistXmlParser(_logger, _providerManager, XmlReaderSettingsFactory, FileSystem).Fetch(result, path, cancellationToken); + new PlaylistXmlParser(_logger, _providerManager).Fetch(result, path, cancellationToken); } protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 438b842525..30a33b729a 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Savers @@ -23,7 +22,7 @@ namespace MediaBrowser.LocalMetadata.Savers { private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) { FileSystem = fileSystem; ConfigurationManager = configurationManager; @@ -31,7 +30,6 @@ namespace MediaBrowser.LocalMetadata.Savers UserManager = userManager; UserDataManager = userDataManager; Logger = logger; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected IFileSystem FileSystem { get; private set; } @@ -40,9 +38,6 @@ namespace MediaBrowser.LocalMetadata.Savers protected IUserManager UserManager { get; private set; } protected IUserDataManager UserDataManager { get; private set; } protected ILogger Logger { get; private set; } - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - - protected ItemUpdateType MinimumUpdateType => ItemUpdateType.MetadataDownload; public string Name => XmlProviderUtils.Name; diff --git a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs index b4d5440a52..ea939e33b1 100644 --- a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Savers @@ -31,7 +30,8 @@ namespace MediaBrowser.LocalMetadata.Savers return Path.Combine(item.Path, "collection.xml"); } - public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger, xmlReaderSettingsFactory) + public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } } diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index 09bb6d8f72..35a431fa48 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Savers @@ -49,7 +48,8 @@ namespace MediaBrowser.LocalMetadata.Savers return Path.Combine(path, "playlist.xml"); } - public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger, xmlReaderSettingsFactory) + public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { } } diff --git a/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs b/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs deleted file mode 100644 index b39325958b..0000000000 --- a/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Xml; - -namespace MediaBrowser.Model.Xml -{ - public interface IXmlReaderSettingsFactory - { - XmlReaderSettings Create(bool enableValidation); - } -} diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index e4bb52217c..335f2c5395 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Music @@ -28,17 +27,15 @@ namespace MediaBrowser.Providers.Music private readonly IApplicationHost _appHost; private readonly ILogger _logger; private readonly IJsonSerializer _json; - private readonly IXmlReaderSettingsFactory _xmlSettings; public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org"; - public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json, IXmlReaderSettingsFactory xmlSettings) + public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json) { _httpClient = httpClient; _appHost = appHost; _logger = logger; _json = json; - _xmlSettings = xmlSettings; Current = this; } @@ -101,11 +98,13 @@ namespace MediaBrowser.Providers.Music { using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; using (var reader = XmlReader.Create(oReader, settings)) { @@ -240,22 +239,20 @@ namespace MediaBrowser.Providers.Music artistId); using (var response = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) + using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) + var settings = new XmlReaderSettings() { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) - { - var settings = _xmlSettings.Create(false); + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var reader = XmlReader.Create(oReader, settings)) - { - return ReleaseResult.Parse(reader).FirstOrDefault(); - } - } + using (var reader = XmlReader.Create(oReader, settings)) + { + return ReleaseResult.Parse(reader).FirstOrDefault(); } } } @@ -267,22 +264,20 @@ namespace MediaBrowser.Providers.Music WebUtility.UrlEncode(artistName)); using (var response = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) + using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) + var settings = new XmlReaderSettings() { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) - { - var settings = _xmlSettings.Create(false); + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var reader = XmlReader.Create(oReader, settings)) - { - return ReleaseResult.Parse(reader).FirstOrDefault(); - } - } + using (var reader = XmlReader.Create(oReader, settings)) + { + return ReleaseResult.Parse(reader).FirstOrDefault(); } } } @@ -590,26 +585,24 @@ namespace MediaBrowser.Providers.Music var url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); using (var response = await GetMusicBrainzResponse(url, true, true, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) + using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) + var settings = new XmlReaderSettings() { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using (var reader = XmlReader.Create(oReader, settings)) + { + var result = ReleaseResult.Parse(reader).FirstOrDefault(); + + if (result != null) { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var reader = XmlReader.Create(oReader, settings)) - { - var result = ReleaseResult.Parse(reader).FirstOrDefault(); - - if (result != null) - { - return result.ReleaseId; - } - } + return result.ReleaseId; } } } @@ -628,56 +621,54 @@ namespace MediaBrowser.Providers.Music var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId); using (var response = await GetMusicBrainzResponse(url, false, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) + using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) + var settings = new XmlReaderSettings() { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using (var reader = XmlReader.Create(oReader, settings)) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - - using (var reader = XmlReader.Create(oReader, settings)) + if (reader.NodeType == XmlNodeType.Element) { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + switch (reader.Name) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) + case "release-group-list": { - case "release-group-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subReader = reader.ReadSubtree()) - { - return GetFirstReleaseGroupId(subReader); - } - } - default: - { - reader.Skip(); - break; - } + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + using (var subReader = reader.ReadSubtree()) + { + return GetFirstReleaseGroupId(subReader); + } + } + default: + { + reader.Skip(); + break; } - } - else - { - reader.Read(); - } } - return null; + } + else + { + reader.Read(); } } + return null; } } } diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs index 8d10834c32..924b712507 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs @@ -13,17 +13,14 @@ using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Xml; namespace MediaBrowser.Providers.Music { public class MusicBrainzArtistProvider : IRemoteMetadataProvider { - private readonly IXmlReaderSettingsFactory _xmlSettings; - - public MusicBrainzArtistProvider(IXmlReaderSettingsFactory xmlSettings) + public MusicBrainzArtistProvider() { - _xmlSettings = xmlSettings; + } public async Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) @@ -84,11 +81,13 @@ namespace MediaBrowser.Providers.Music { using (var oReader = new StreamReader(stream, Encoding.UTF8)) { - var settings = _xmlSettings.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; using (var reader = XmlReader.Create(oReader, settings)) { diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index 0a2975e0f9..752c0941d0 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index afbd838e4b..d5b0b6fd89 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.TV.TheTVDB; using Microsoft.Extensions.Logging; @@ -18,7 +17,6 @@ namespace MediaBrowser.Providers.TV public class SeriesMetadataService : MetadataService { private readonly ILocalizationManager _localization; - private readonly IXmlReaderSettingsFactory _xmlSettings; private readonly TvDbClientManager _tvDbClientManager; public SeriesMetadataService( @@ -29,13 +27,11 @@ namespace MediaBrowser.Providers.TV IUserDataManager userDataManager, ILibraryManager libraryManager, ILocalizationManager localization, - IXmlReaderSettingsFactory xmlSettings, TvDbClientManager tvDbClientManager ) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) { _localization = localization; - _xmlSettings = xmlSettings; _tvDbClientManager = tvDbClientManager; } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs new file mode 100644 index 0000000000..c8ae585740 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Providers.TV.TheTVDB; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.TV +{ + /// + /// Class TvdbPrescanTask + /// + public class TvdbPrescanTask : ILibraryPostScanTask + { + public const string TvdbBaseUrl = "https://thetvdb.com/"; + + /// + /// The server time URL + /// + private const string ServerTimeUrl = TvdbBaseUrl + "api/Updates.php?type=none"; + + /// + /// The updates URL + /// + private const string UpdatesUrl = TvdbBaseUrl + "api/Updates.php?type=all&time={0}"; + + /// + /// The _HTTP client + /// + private readonly IHttpClient _httpClient; + /// + /// The _logger + /// + private readonly ILogger _logger; + /// + /// The _config + /// + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The HTTP client. + /// The config. + public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager) + { + _logger = logger; + _httpClient = httpClient; + _config = config; + _fileSystem = fileSystem; + _libraryManager = libraryManager; + } + + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); + + Directory.CreateDirectory(path); + + var timestampFile = Path.Combine(path, "time.txt"); + + var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); + + // Don't check for tvdb updates anymore frequently than 24 hours + if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1) + { + return; + } + + // Find out the last time we queried tvdb for updates + var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; + + string newUpdateTime; + + var existingDirectories = _fileSystem.GetDirectoryPaths(path) + .Select(Path.GetFileName) + .ToList(); + + var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + GroupByPresentationUniqueKey = false, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }).Cast() + .ToList(); + + var seriesIdsInLibrary = seriesList + .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) + .Select(i => i.GetProviderId(MetadataProviders.Tvdb)) + .ToList(); + + var missingSeries = seriesIdsInLibrary.Except(existingDirectories, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var enableInternetProviders = seriesList.Count == 0 ? false : seriesList[0].IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(seriesList[0]), TvdbSeriesProvider.Current.Name); + if (!enableInternetProviders) + { + progress.Report(100); + return; + } + + // If this is our first time, update all series + if (string.IsNullOrEmpty(lastUpdateTime)) + { + // First get tvdb server time + using (var response = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = ServerTimeUrl, + CancellationToken = cancellationToken, + EnableHttpCompression = true, + BufferContent = false + + }, "GET").ConfigureAwait(false)) + { + // First get tvdb server time + using (var stream = response.Content) + { + newUpdateTime = GetUpdateTime(stream); + } + } + + existingDirectories.AddRange(missingSeries); + + await UpdateSeries(existingDirectories, path, null, progress, cancellationToken).ConfigureAwait(false); + } + else + { + var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); + + newUpdateTime = seriesToUpdate.Item2; + + long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out var lastUpdateValue); + + var nullableUpdateValue = lastUpdateValue == 0 ? (long?)null : lastUpdateValue; + + var listToUpdate = seriesToUpdate.Item1.ToList(); + listToUpdate.AddRange(missingSeries); + + await UpdateSeries(listToUpdate, path, nullableUpdateValue, progress, cancellationToken).ConfigureAwait(false); + } + + File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); + progress.Report(100); + } + + /// + /// Gets the update time. + /// + /// The response. + /// System.String. + private string GetUpdateTime(Stream response) + { + // Use XmlReader for best performance + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using (var streamReader = new StreamReader(response, Encoding.UTF8)) + using (var reader = XmlReader.Create(streamReader, settings)) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Time": + { + return (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + } + default: + reader.Skip(); + break; + } + } + else + { + reader.Read(); + } + } + } + + return null; + } + + /// + /// Gets the series ids to update. + /// + /// The existing series ids. + /// The last update time. + /// The cancellation token. + /// Task{IEnumerable{System.String}}. + private async Task, string>> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken) + { + // First get last time + using (var response = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = string.Format(UpdatesUrl, lastUpdateTime), + CancellationToken = cancellationToken, + EnableHttpCompression = true, + BufferContent = false + + }, "GET").ConfigureAwait(false)) + { + using (var stream = response.Content) + { + var data = GetUpdatedSeriesIdList(stream); + + var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + var seriesList = data.ids + .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); + + return new Tuple, string>(seriesList, data.updateTime); + } + } + } + + private (List ids, string updateTime) GetUpdatedSeriesIdList(Stream stream) + { + string updateTime = null; + var idList = new List(); + + // Use XmlReader for best performance + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using (var streamReader = new StreamReader(stream, Encoding.UTF8)) + using (var reader = XmlReader.Create(streamReader, settings)) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Time": + { + updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + case "Series": + { + var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + idList.Add(id); + break; + } + default: + reader.Skip(); + break; + } + } + else + { + reader.Read(); + } + } + } + + return (idList, updateTime); + } + + /// + /// Updates the series. + /// + /// The series ids. + /// The series data path. + /// The last tv db update time. + /// The progress. + /// The cancellation token. + /// Task. + private async Task UpdateSeries(List seriesIds, string seriesDataPath, long? lastTvDbUpdateTime, IProgress progress, CancellationToken cancellationToken) + { + var numComplete = 0; + + var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + GroupByPresentationUniqueKey = false, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }).Cast(); + + // Gather all series into a lookup by tvdb id + var allSeries = seriesList + .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) + .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb)); + + foreach (var seriesId in seriesIds) + { + // Find the preferred language(s) for the movie in the library + var languages = allSeries[seriesId] + .Select(i => i.GetPreferredMetadataLanguage()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var language in languages) + { + try + { + await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + _logger.LogError(ex, "Error updating tvdb series id {ID}, language {Language}", seriesId, language); + + // Already logged at lower levels, but don't fail the whole operation, unless timed out + // We have to fail this to make it run again otherwise new episode data could potentially be missing + if (ex.IsTimedOut) + { + throw; + } + } + } + + numComplete++; + double percent = numComplete; + percent /= seriesIds.Count; + percent *= 100; + + progress.Report(percent); + } + } + + /// + /// Updates the series. + /// + /// The id. + /// The series data path. + /// The last tv db update time. + /// The preferred metadata language. + /// The cancellation token. + /// Task. + private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + _logger.LogInformation("Updating series from tvdb " + id + ", language " + preferredMetadataLanguage); + + seriesDataPath = Path.Combine(seriesDataPath, id); + + Directory.CreateDirectory(seriesDataPath); + + return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), null, null, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index 9c24e4c987..09b056c0cb 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -23,7 +23,6 @@ namespace MediaBrowser.Providers.TV.TheTVDB { internal static TvdbSeriesProvider Current { get; private set; } private readonly IHttpClient _httpClient; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index f20dbbb6ea..aa5313eb72 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -13,8 +13,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.XbmcMetadata.Configuration; using MediaBrowser.XbmcMetadata.Savers; using Microsoft.Extensions.Logging; @@ -28,9 +26,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// The logger /// protected ILogger Logger { get; private set; } - protected IFileSystem FileSystem { get; private set; } protected IProviderManager ProviderManager { get; private set; } - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IConfigurationManager _config; @@ -39,13 +35,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// /// Initializes a new instance of the class. /// - public BaseNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public BaseNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) { Logger = logger; _config = config; ProviderManager = providerManager; - FileSystem = fileSystem; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } /// @@ -68,11 +62,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers throw new ArgumentException("The metadata file was empty or null.", nameof(metadataFile)); } - var settings = XmlReaderSettingsFactory.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; _validProviderIds = _validProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -935,19 +931,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers value = value.Trim().Trim(separator); - return string.IsNullOrWhiteSpace(value) ? Array.Empty() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries); - } - - /// - /// Provides an additional overload for string.split - /// - /// The val. - /// The separators. - /// The options. - /// System.String[][]. - private string[] Split(string val, char[] separators, StringSplitOptions options) - { - return val.Split(separators, options); + return string.IsNullOrWhiteSpace(value) ? Array.Empty() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index a8cb6ac40d..7f68ba256e 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -8,8 +8,6 @@ using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers @@ -220,7 +218,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, config, providerManager, fileSystem, xmlReaderSettingsFactory) + public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) { } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index a3e48d30d9..526cc62b0e 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -7,8 +7,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers @@ -124,14 +122,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers using (var stringReader = new StringReader("" + xml + "")) { // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + try { - var settings = XmlReaderSettingsFactory.Create(false); - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - // Use XmlReader for best performance using (var reader = XmlReader.Create(stringReader, settings)) { @@ -167,7 +167,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, config, providerManager, fileSystem, xmlReaderSettingsFactory) + public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) { } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 17f36d82d7..882f3a9d3a 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -3,8 +3,6 @@ using System.Xml; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers @@ -42,7 +40,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, config, providerManager, fileSystem, xmlReaderSettingsFactory) + public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) { } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 700656b652..b0f25ae64e 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -5,8 +5,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers @@ -94,7 +92,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, config, providerManager, fileSystem, xmlReaderSettingsFactory) + public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) { } } diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs index 1e22bf3582..6e6a22794d 100644 --- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs @@ -4,7 +4,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging; @@ -15,20 +14,18 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public AlbumNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public AlbumNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _config = config; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) { - new BaseNfoParser(_logger, _config, _providerManager, FileSystem, XmlReaderSettingsFactory).Fetch(result, path, cancellationToken); + new BaseNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs index ad811b4754..20abfc7f3a 100644 --- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs @@ -4,7 +4,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging; @@ -15,20 +14,18 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public ArtistNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public ArtistNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _config = config; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) { - new BaseNfoParser(_logger, _config, _providerManager, FileSystem, XmlReaderSettingsFactory).Fetch(result, path, cancellationToken); + new BaseNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index 94f104f61b..28a0514d57 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -4,7 +4,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.XbmcMetadata.Parsers; using MediaBrowser.XbmcMetadata.Savers; using Microsoft.Extensions.Logging; @@ -17,15 +16,13 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public BaseVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public BaseVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _config = config; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) @@ -34,7 +31,7 @@ namespace MediaBrowser.XbmcMetadata.Providers { Item = result.Item }; - new MovieNfoParser(_logger, _config, _providerManager, FileSystem, XmlReaderSettingsFactory).Fetch(tmpItem, path, cancellationToken); + new MovieNfoParser(_logger, _config, _providerManager).Fetch(tmpItem, path, cancellationToken); result.Item = (T)tmpItem.Item; result.People = tmpItem.People; diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs index bf05f0f38b..f90f283cf3 100644 --- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs @@ -5,7 +5,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging; @@ -16,22 +15,20 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; - protected IXmlReaderSettingsFactory XmlReaderSettingsFactory { get; private set; } - public EpisodeNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + public EpisodeNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) : base(fileSystem) { _logger = logger; _config = config; _providerManager = providerManager; - XmlReaderSettingsFactory = xmlReaderSettingsFactory; } protected override void Fetch(MetadataResult result, string path, CancellationToken cancellationToken) { var images = new List(); - new EpisodeNfoParser(_logger, _config, _providerManager, FileSystem, XmlReaderSettingsFactory).Fetch(result, images, path, cancellationToken); + new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, images, path, cancellationToken); result.Images = images; } diff --git a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs index 77b3b3781b..d21164c022 100644 --- a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs @@ -3,28 +3,30 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Providers { public class MovieNfoProvider : BaseVideoNfoProvider { - public MovieNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(fileSystem, logger, config, providerManager, xmlReaderSettingsFactory) + public MovieNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(fileSystem, logger, config, providerManager) { } } public class MusicVideoNfoProvider : BaseVideoNfoProvider { - public MusicVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(fileSystem, logger, config, providerManager, xmlReaderSettingsFactory) + public MusicVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(fileSystem, logger, config, providerManager) { } } public class VideoNfoProvider : BaseVideoNfoProvider public class TypeMapper { - private readonly IAssemblyInfo _assemblyInfo; - /// /// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types /// private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); - public TypeMapper(IAssemblyInfo assemblyInfo) + public TypeMapper() { - _assemblyInfo = assemblyInfo; } /// @@ -45,8 +41,7 @@ namespace Emby.Server.Implementations.Data /// Type. private Type LookupType(string typeName) { - return _assemblyInfo - .GetCurrentAssemblies() + return AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(typeName)) .FirstOrDefault(t => t != null); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index fceb82ba19..f4eddb08ba 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -33,7 +33,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IProviderManager _providerManager; private readonly IMediaEncoder _mediaEncoder; private readonly IProcessFactory _processFactory; - private readonly IAssemblyInfo _assemblyInfo; private IMediaSourceManager _mediaSourceManager; public static EmbyTV Current; @@ -74,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public EmbyTV(IServerApplicationHost appHost, IStreamHelper streamHelper, IMediaSourceManager mediaSourceManager, - IAssemblyInfo assemblyInfo, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, @@ -101,7 +98,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _processFactory = processFactory; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _assemblyInfo = assemblyInfo; _mediaSourceManager = mediaSourceManager; _streamHelper = streamHelper; diff --git a/Emby.Server.Implementations/Reflection/AssemblyInfo.cs b/Emby.Server.Implementations/Reflection/AssemblyInfo.cs deleted file mode 100644 index 9d16fe43f6..0000000000 --- a/Emby.Server.Implementations/Reflection/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using MediaBrowser.Model.Reflection; - -namespace Emby.Server.Implementations.Reflection -{ - public class AssemblyInfo : IAssemblyInfo - { - public Stream GetManifestResourceStream(Type type, string resource) - { - return type.Assembly.GetManifestResourceStream(resource); - } - - public string[] GetManifestResourceNames(Type type) - { - return type.Assembly.GetManifestResourceNames(); - } - - public Assembly[] GetCurrentAssemblies() - { - return AppDomain.CurrentDomain.GetAssemblies(); - } - } -} diff --git a/MediaBrowser.Model/Reflection/IAssemblyInfo.cs b/MediaBrowser.Model/Reflection/IAssemblyInfo.cs deleted file mode 100644 index 5c4536c1c1..0000000000 --- a/MediaBrowser.Model/Reflection/IAssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.IO; -using System.Reflection; - -namespace MediaBrowser.Model.Reflection -{ - public interface IAssemblyInfo - { - Stream GetManifestResourceStream(Type type, string resource); - string[] GetManifestResourceNames(Type type); - - Assembly[] GetCurrentAssemblies(); - } -} diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 531978e1dd..aa0208495b 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -13,7 +13,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; @@ -117,20 +116,26 @@ namespace MediaBrowser.WebDashboard.Api private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; private readonly IJsonSerializer _jsonSerializer; - private readonly IAssemblyInfo _assemblyInfo; private IResourceFileManager _resourceFileManager; /// /// Initializes a new instance of the class. /// - public DashboardService(IServerApplicationHost appHost, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, IAssemblyInfo assemblyInfo, ILogger logger, IHttpResultFactory resultFactory) + public DashboardService( + IServerApplicationHost appHost, + IResourceFileManager resourceFileManager, + IServerConfigurationManager serverConfigurationManager, + IFileSystem fileSystem, + ILocalizationManager localization, + IJsonSerializer jsonSerializer, + ILogger logger, + IHttpResultFactory resultFactory) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; _localization = localization; _jsonSerializer = jsonSerializer; - _assemblyInfo = assemblyInfo; _logger = logger; _resultFactory = resultFactory; _resourceFileManager = resourceFileManager; @@ -187,7 +192,7 @@ namespace MediaBrowser.WebDashboard.Api if (altPage != null) { plugin = altPage.Item2; - stream = _assemblyInfo.GetManifestResourceStream(plugin.GetType(), altPage.Item1.EmbeddedResourcePath); + stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html"); From f486f5966f2fb9a3cf266ee816b8c247f0de5482 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 09:56:03 -0800 Subject: [PATCH 0172/6208] Update Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultAuthenticationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 526509f439..3ec1f81d31 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.Library } else { - throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); + throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}")); } // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); From 8c609bc9ce9f548161c3d950706653a525f7e1de Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 7 Mar 2019 19:04:09 +0100 Subject: [PATCH 0173/6208] Reduce aspnet imports --- Emby.Dlna/ControlRequest.cs | 1 - Emby.Dlna/Emby.Dlna.csproj | 5 +++ Emby.Dlna/PlayTo/PlayToController.cs | 1 - .../MediaBrowser.Common.csproj | 3 +- .../Net/WebSocketConnectEventArgs.cs | 41 ------------------- MediaBrowser.Model/MediaBrowser.Model.csproj | 5 +-- 6 files changed, 9 insertions(+), 47 deletions(-) delete mode 100644 MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index 907d437f85..8c227159c4 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Http; diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 71ded23373..4c07087c53 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -58,4 +58,9 @@ + + + + + diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 8d7d59249a..67d5cfef42 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -21,7 +21,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; namespace Emby.Dlna.PlayTo { diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 715f4fccd3..05b48a2a12 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors @@ -13,6 +13,7 @@ + diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index 107e674216..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Internal; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Class WebSocketConnectEventArgs - /// - public class WebSocketConnectingEventArgs : EventArgs - { - /// - /// Gets or sets the URL. - /// - /// The URL. - public string Url { get; set; } - /// - /// Gets or sets the endpoint. - /// - /// The endpoint. - public string Endpoint { get; set; } - /// - /// Gets or sets the query string. - /// - /// The query string. - public IQueryCollection QueryString { get; set; } - /// - /// Gets or sets a value indicating whether [allow connection]. - /// - /// true if [allow connection]; otherwise, false. - public bool AllowConnection { get; set; } - - public WebSocketConnectingEventArgs() - { - QueryString = new QueryCollection(); - AllowConnection = true; - } - } - -} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 2f1ea13d9b..3de2cca2d2 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors @@ -13,8 +13,7 @@ - - + From e4c5d51860b5e421abac8d453c4655188074501d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 19:04:50 +0100 Subject: [PATCH 0174/6208] Update MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs Co-Authored-By: cvium --- .../Playback/Progressive/BaseProgressiveStreamingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 399ac2be09..f43f26b2f3 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -366,7 +366,7 @@ namespace MediaBrowser.Api.Playback.Progressive // Headers only if (isHeadRequest) { - return ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders); + return ResultFactory.GetResult(null, Array.Empty(), contentType, responseHeaders); } var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); From dfff68b2f4ca8d3da59508eb4cbe2e751ed76f49 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 7 Mar 2019 19:05:47 +0100 Subject: [PATCH 0175/6208] Make SkipLogExtensions static --- Emby.Server.Implementations/HttpServer/FileWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 0e989994b1..c4d2a70e23 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private readonly string[] SkipLogExtensions = { + private static readonly string[] SkipLogExtensions = { ".js", ".html", ".css" From 65c0b486aaa6d02855a6915edfb880da691b4709 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 19:27:43 +0100 Subject: [PATCH 0176/6208] Remove dead code What it says on the tin --- DvdLib/Ifo/AudioAttributes.cs | 36 ---------------- DvdLib/Ifo/PgcCommandTable.cs | 17 -------- DvdLib/Ifo/ProgramChain.cs | 4 -- DvdLib/Ifo/VideoAttributes.cs | 46 -------------------- Emby.Dlna/PlayTo/CurrentIdEventArgs.cs | 9 ---- Emby.Dlna/PlayTo/PlaylistItemFactory.cs | 2 - Emby.Dlna/PlayTo/TransportStateEventArgs.cs | 9 ---- Emby.Dlna/PlayTo/uParser.cs | 47 --------------------- Emby.Dlna/PlayTo/uParserObject.cs | 9 ---- 9 files changed, 179 deletions(-) delete mode 100644 DvdLib/Ifo/AudioAttributes.cs delete mode 100644 DvdLib/Ifo/PgcCommandTable.cs delete mode 100644 DvdLib/Ifo/VideoAttributes.cs delete mode 100644 Emby.Dlna/PlayTo/CurrentIdEventArgs.cs delete mode 100644 Emby.Dlna/PlayTo/TransportStateEventArgs.cs delete mode 100644 Emby.Dlna/PlayTo/uParser.cs delete mode 100644 Emby.Dlna/PlayTo/uParserObject.cs diff --git a/DvdLib/Ifo/AudioAttributes.cs b/DvdLib/Ifo/AudioAttributes.cs deleted file mode 100644 index b76f9fc05e..0000000000 --- a/DvdLib/Ifo/AudioAttributes.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace DvdLib.Ifo -{ - public enum AudioCodec - { - AC3 = 0, - MPEG1 = 2, - MPEG2ext = 3, - LPCM = 4, - DTS = 6, - } - - public enum ApplicationMode - { - Unspecified = 0, - Karaoke = 1, - Surround = 2, - } - - public class AudioAttributes - { - public readonly AudioCodec Codec; - public readonly bool MultichannelExtensionPresent; - public readonly ApplicationMode Mode; - public readonly byte QuantDRC; - public readonly byte SampleRate; - public readonly byte Channels; - public readonly ushort LanguageCode; - public readonly byte LanguageExtension; - public readonly byte CodeExtension; - } - - public class MultiChannelExtension - { - - } -} diff --git a/DvdLib/Ifo/PgcCommandTable.cs b/DvdLib/Ifo/PgcCommandTable.cs deleted file mode 100644 index d329fcba2a..0000000000 --- a/DvdLib/Ifo/PgcCommandTable.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -namespace DvdLib.Ifo -{ - public class ProgramChainCommandTable - { - public readonly ushort LastByteAddress; - public readonly List PreCommands; - public readonly List PostCommands; - public readonly List CellCommands; - } - - public class VirtualMachineCommand - { - public readonly byte[] Command; - } -} diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs index 80889738f1..7b003005b9 100644 --- a/DvdLib/Ifo/ProgramChain.cs +++ b/DvdLib/Ifo/ProgramChain.cs @@ -25,13 +25,10 @@ namespace DvdLib.Ifo public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries private ushort _nextProgramNumber; - public readonly ProgramChain Next; private ushort _prevProgramNumber; - public readonly ProgramChain Previous; private ushort _goupProgramNumber; - public readonly ProgramChain Goup; // ?? maybe Group public ProgramPlaybackMode PlaybackMode { get; private set; } public uint ProgramCount { get; private set; } @@ -40,7 +37,6 @@ namespace DvdLib.Ifo public byte[] Palette { get; private set; } // 16*4 entries private ushort _commandTableOffset; - public readonly ProgramChainCommandTable CommandTable; private ushort _programMapOffset; private ushort _cellPlaybackOffset; diff --git a/DvdLib/Ifo/VideoAttributes.cs b/DvdLib/Ifo/VideoAttributes.cs deleted file mode 100644 index 8b3996715c..0000000000 --- a/DvdLib/Ifo/VideoAttributes.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace DvdLib.Ifo -{ - public enum VideoCodec - { - MPEG1 = 0, - MPEG2 = 1, - } - - public enum VideoFormat - { - NTSC = 0, - PAL = 1, - } - - public enum AspectRatio - { - ar4to3 = 0, - ar16to9 = 3 - } - - public enum FilmMode - { - None = -1, - Camera = 0, - Film = 1, - } - - public class VideoAttributes - { - public readonly VideoCodec Codec; - public readonly VideoFormat Format; - public readonly AspectRatio Aspect; - public readonly bool AutomaticPanScan; - public readonly bool AutomaticLetterBox; - public readonly bool Line21CCField1; - public readonly bool Line21CCField2; - public readonly int Width; - public readonly int Height; - public readonly bool Letterboxed; - public readonly FilmMode FilmMode; - - public VideoAttributes() - { - } - } -} diff --git a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs b/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs deleted file mode 100644 index fdf435bcf3..0000000000 --- a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class CurrentIdEventArgs : EventArgs - { - public string Id { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index aceb634e33..446d8e1e6e 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -9,8 +9,6 @@ namespace Emby.Dlna.PlayTo { public class PlaylistItemFactory { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public PlaylistItem Create(Photo item, DeviceProfile profile) { var playlistItem = new PlaylistItem diff --git a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs b/Emby.Dlna/PlayTo/TransportStateEventArgs.cs deleted file mode 100644 index 7dcd39e107..0000000000 --- a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class TransportStateEventArgs : EventArgs - { - public TRANSPORTSTATE State { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/uParser.cs b/Emby.Dlna/PlayTo/uParser.cs deleted file mode 100644 index 3a0ffffd41..0000000000 --- a/Emby.Dlna/PlayTo/uParser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParser - { - public static IList ParseBrowseXml(XDocument doc) - { - if (doc == null) - { - throw new ArgumentException("doc"); - } - - var list = new List(); - - var document = doc.Document; - - if (document == null) - return list; - - var item = (from result in document.Descendants("Result") select result).FirstOrDefault(); - - if (item == null) - return list; - - var uPnpResponse = XElement.Parse((string)item); - - var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers) - select new uParserObject { Element = container }; - - var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items) - select new uParserObject { Element = container }; - - list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null)); - - return list; - } - - public static uBaseObject CreateObjectFromXML(uParserObject uItem) - { - return UpnpContainer.Create(uItem.Element); - } - } -} diff --git a/Emby.Dlna/PlayTo/uParserObject.cs b/Emby.Dlna/PlayTo/uParserObject.cs deleted file mode 100644 index 87a7f69c62..0000000000 --- a/Emby.Dlna/PlayTo/uParserObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParserObject - { - public XElement Element { get; set; } - } -} From c5fce647defd2eace2d2431ccc52ffe1d027c184 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 15:54:30 +0100 Subject: [PATCH 0177/6208] Cleanup/simplification * Removed useless copies/allocations * Reduced unneeded complexity --- .../Collections/CollectionImageProvider.cs | 4 +- .../IO/ManagedFileSystem.cs | 7 +- .../Images/BaseDynamicImageProvider.cs | 58 +++++------- .../Playlists/PlaylistImageProvider.cs | 13 +-- .../SocketSharp/RequestMono.cs | 17 ++-- .../SocketSharp/WebSocketSharpRequest.cs | 4 +- .../CollectionFolderImageProvider.cs | 9 +- .../UserViews/DynamicImageProvider.cs | 13 +-- .../UserViews/FolderImageProvider.cs | 4 +- MediaBrowser.Api/Images/ImageService.cs | 91 ++++++++++--------- MediaBrowser.Controller/Entities/BaseItem.cs | 26 ++++-- MediaBrowser.Controller/Entities/Folder.cs | 2 +- .../Entities/ItemImageInfo.cs | 16 +--- .../Entities/Movies/BoxSet.cs | 2 +- .../Entities/Movies/Movie.cs | 8 +- .../Entities/TV/Episode.cs | 2 +- MediaBrowser.Controller/Entities/TV/Series.cs | 3 +- MediaBrowser.Controller/Entities/Video.cs | 2 +- MediaBrowser.Model/IO/IFileSystem.cs | 3 +- 19 files changed, 127 insertions(+), 157 deletions(-) diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index cdfb5cadf1..0244c4a684 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Collections return base.Supports(item); } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var playlist = (BoxSet)item; @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Collections .ToList(); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index a64dfb607b..421592fada 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -7,7 +7,6 @@ using System.Text; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -711,20 +710,20 @@ namespace Emby.Server.Implementations.IO return GetFiles(path, null, false, recursive); } - public virtual IEnumerable GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1) { return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); } var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); - if (extensions != null && extensions.Length > 0) + if (extensions != null && extensions.Count > 0) { files = files.Where(i => { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 109c21f18d..46f209b4b4 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -20,6 +20,9 @@ namespace Emby.Server.Implementations.Images public abstract class BaseDynamicImageProvider : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider, IHasOrder where T : BaseItem { + protected virtual IReadOnlyCollection SupportedImages { get; } + = new ImageType[] { ImageType.Primary }; + protected IFileSystem FileSystem { get; private set; } protected IProviderManager ProviderManager { get; private set; } protected IApplicationPaths ApplicationPaths { get; private set; } @@ -33,18 +36,7 @@ namespace Emby.Server.Implementations.Images ImageProcessor = imageProcessor; } - protected virtual bool Supports(BaseItem item) - { - return true; - } - - public virtual ImageType[] GetSupportedImages(BaseItem item) - { - return new ImageType[] - { - ImageType.Primary - }; - } + protected virtual bool Supports(BaseItem _) => true; public async Task FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) { @@ -54,15 +46,14 @@ namespace Emby.Server.Implementations.Images } var updateType = ItemUpdateType.None; - var supportedImages = GetSupportedImages(item); - if (supportedImages.Contains(ImageType.Primary)) + if (SupportedImages.Contains(ImageType.Primary)) { var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false); updateType = updateType | primaryResult; } - if (supportedImages.Contains(ImageType.Thumb)) + if (SupportedImages.Contains(ImageType.Thumb)) { var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false); updateType = updateType | thumbResult; @@ -94,7 +85,7 @@ namespace Emby.Server.Implementations.Images } protected async Task FetchToFileInternal(BaseItem item, - List itemsWithImages, + IReadOnlyList itemsWithImages, ImageType imageType, CancellationToken cancellationToken) { @@ -119,9 +110,9 @@ namespace Emby.Server.Implementations.Images return ItemUpdateType.ImageUpdate; } - protected abstract List GetItemsWithImages(BaseItem item); + protected abstract IReadOnlyList GetItemsWithImages(BaseItem item); - protected string CreateThumbCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 640, 360); } @@ -132,38 +123,38 @@ namespace Emby.Server.Implementations.Images .Select(i => { var image = i.GetImageInfo(ImageType.Primary, 0); - if (image != null && image.IsLocalFile) { return image.Path; } + image = i.GetImageInfo(ImageType.Thumb, 0); - if (image != null && image.IsLocalFile) { return image.Path; } + return null; }) .Where(i => !string.IsNullOrEmpty(i)); } - protected string CreatePosterCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreatePosterCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 400, 600); } - protected string CreateSquareCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreateSquareCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 600, 600); } - protected string CreateThumbCollage(BaseItem primaryItem, List items, string outputPath, int width, int height) + protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable items, string outputPath, int width, int height) { return CreateCollage(primaryItem, items, outputPath, width, height); } - private string CreateCollage(BaseItem primaryItem, List items, string outputPath, int width, int height) + private string CreateCollage(BaseItem primaryItem, IEnumerable items, string outputPath, int width, int height) { Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); @@ -192,7 +183,7 @@ namespace Emby.Server.Implementations.Images public string Name => "Dynamic Image Provider"; protected virtual string CreateImage(BaseItem item, - List itemsWithImages, + IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) @@ -211,18 +202,15 @@ namespace Emby.Server.Implementations.Images if (imageType == ImageType.Primary) { - if (item is UserView) - { - return CreateSquareCollage(item, itemsWithImages, outputPath); - } - if (item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum) + if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum) { return CreateSquareCollage(item, itemsWithImages, outputPath); } + return CreatePosterCollage(item, itemsWithImages, outputPath); } - throw new ArgumentException("Unexpected image type"); + throw new ArgumentException("Unexpected image type", nameof(imageType)); } protected virtual int MaxImageAgeDays => 7; @@ -234,13 +222,11 @@ namespace Emby.Server.Implementations.Images return false; } - var supportedImages = GetSupportedImages(item); - - if (supportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary)) + if (SupportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary)) { return true; } - if (supportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) + if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) { return true; } @@ -285,7 +271,7 @@ namespace Emby.Server.Implementations.Images public int Order => 0; - protected string CreateSingleImage(List itemsWithImages, string outputPathWithoutExtension, ImageType imageType) + protected string CreateSingleImage(IEnumerable itemsWithImages, string outputPathWithoutExtension, ImageType imageType) { var image = itemsWithImages .Where(i => i.HasImage(imageType) && i.GetImageInfo(imageType, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(imageType))) diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index 8a7c1492d4..cad66a80fd 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Playlists { } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var playlist = (Playlist)item; @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.Playlists Recursive = true, ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }); } } @@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -116,11 +115,5 @@ namespace Emby.Server.Implementations.Playlists DtoOptions = new DtoOptions(false) }); } - - //protected override Task CreateImage(IHasMetadata item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - //{ - // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - //} } - } diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index 5e29e40584..373f6d7580 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.SocketSharp byte[] copy = new byte[e.Length]; input.Position = e.Start; - input.Read(copy, 0, (int)e.Length); + await input.ReadAsync(copy, 0, (int)e.Length).ConfigureAwait(false); form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); } @@ -98,11 +98,11 @@ namespace Emby.Server.Implementations.SocketSharp var form = new WebROCollection(); files = new Dictionary(); - if (IsContentType("multipart/form-data", true)) + if (IsContentType("multipart/form-data")) { await LoadMultiPart(form).ConfigureAwait(false); } - else if (IsContentType("application/x-www-form-urlencoded", true)) + else if (IsContentType("application/x-www-form-urlencoded")) { await LoadWwwForm(form).ConfigureAwait(false); } @@ -200,19 +200,14 @@ namespace Emby.Server.Implementations.SocketSharp return false; } - private bool IsContentType(string ct, bool starts_with) + private bool IsContentType(string ct) { - if (ct == null || ContentType == null) + if (ContentType == null) { return false; } - if (starts_with) - { - return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); + return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); } private async Task LoadWwwForm(WebROCollection form) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index bc002dc4ce..10b7c45149 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Net; using System.Text; -using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -405,8 +404,7 @@ namespace Emby.Server.Implementations.SocketSharp return null; } - var path = sbPathInfo.ToString(); - return path.Length > 1 ? path.TrimEnd('/') : "/"; + return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/"; } public string UserAgent => request.Headers[HeaderNames.UserAgent]; diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index ce6c2cd87d..a3f3f6cb4d 100644 --- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; @@ -20,7 +19,7 @@ namespace Emby.Server.Implementations.UserViews { } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var view = (CollectionFolder)item; var viewType = view.CollectionType; @@ -56,7 +55,7 @@ namespace Emby.Server.Implementations.UserViews includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" }; } - var recursive = !new[] { CollectionType.Playlists }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase); return view.GetItemList(new InternalItemsQuery { @@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.UserViews }, IncludeItemTypes = includeItemTypes - }).ToList(); + }); } protected override bool Supports(BaseItem item) @@ -79,7 +78,7 @@ namespace Emby.Server.Implementations.UserViews return item is CollectionFolder; } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs index 4ec68e550d..f485204436 100644 --- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var view = (UserView)item; @@ -46,8 +46,7 @@ namespace Emby.Server.Implementations.UserViews var items = result.Select(i => { - var episode = i as Episode; - if (episode != null) + if (i is Episode episode) { var series = episode.Series; if (series != null) @@ -58,8 +57,7 @@ namespace Emby.Server.Implementations.UserViews return episode; } - var season = i as Season; - if (season != null) + if (i is Season season) { var series = season.Series; if (series != null) @@ -70,8 +68,7 @@ namespace Emby.Server.Implementations.UserViews return season; } - var audio = i as Audio; - if (audio != null) + if (i is Audio audio) { var album = audio.AlbumEntity; if (album != null && album.HasImage(ImageType.Primary)) @@ -122,7 +119,7 @@ namespace Emby.Server.Implementations.UserViews return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { if (itemsWithImages.Count == 0) { diff --git a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs index c810004ab2..4655cd928a 100644 --- a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.UserViews }); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index c25204bf2d..10bbc9e5d8 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -312,35 +312,35 @@ namespace MediaBrowser.Api.Images private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) { + int? width = null; + int? height = null; + long length = 0; + try { - int? width = null; - int? height = null; - long length = 0; - - try + if (info.IsLocalFile) { - if (info.IsLocalFile) + var fileInfo = _fileSystem.GetFileInfo(info.Path); + length = fileInfo.Length; + + ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true); + width = size.Width; + height = size.Height; + + if (width <= 0 || height <= 0) { - var fileInfo = _fileSystem.GetFileInfo(info.Path); - length = fileInfo.Length; - - ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true); - width = size.Width; - height = size.Height; - - if (width <= 0 || height <= 0) - { - width = null; - height = null; - } - + width = null; + height = null; } } - catch - { + } + catch (Exception ex) + { + Logger.LogError(ex, "Error getting image information for {Item}", item.Name); + } - } + try + { return new ImageInfo { Path = info.Path, @@ -354,7 +354,7 @@ namespace MediaBrowser.Api.Images } catch (Exception ex) { - Logger.LogError(ex, "Error getting image information for {path}", info.Path); + Logger.LogError(ex, "Error getting image information for {Path}", info.Path); return null; } @@ -519,16 +519,16 @@ namespace MediaBrowser.Api.Images request.AddPlayedIndicator = true; } } + if (request.PercentPlayed.HasValue) { request.UnplayedCount = null; } - if (request.UnplayedCount.HasValue) + + if (request.UnplayedCount.HasValue + && request.UnplayedCount.Value <= 0) { - if (request.UnplayedCount.Value <= 0) - { - request.UnplayedCount = null; - } + request.UnplayedCount = null; } if (item == null) @@ -542,7 +542,6 @@ namespace MediaBrowser.Api.Images } var imageInfo = GetImageInfo(request, item); - if (imageInfo == null) { var displayText = item == null ? itemId.ToString() : item.Name; @@ -550,7 +549,6 @@ namespace MediaBrowser.Api.Images } IImageEnhancer[] supportedImageEnhancers; - if (_imageProcessor.ImageEnhancers.Length > 0) { if (item == null) @@ -565,13 +563,15 @@ namespace MediaBrowser.Api.Images supportedImageEnhancers = Array.Empty(); } - var cropwhitespace = request.Type == ImageType.Logo || - request.Type == ImageType.Art; - + bool cropwhitespace; if (request.CropWhitespace.HasValue) { cropwhitespace = request.CropWhitespace.Value; } + else + { + cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art; + } var outputFormats = GetOutputFormats(request); @@ -653,12 +653,10 @@ namespace MediaBrowser.Api.Images private ImageFormat[] GetOutputFormats(ImageRequest request) { - if (!string.IsNullOrWhiteSpace(request.Format)) + if (!string.IsNullOrWhiteSpace(request.Format) + && Enum.TryParse(request.Format, true, out ImageFormat format)) { - if (Enum.TryParse(request.Format, true, out ImageFormat format)) - { - return new ImageFormat[] { format }; - } + return new ImageFormat[] { format }; } return GetClientSupportedFormats(); @@ -666,8 +664,19 @@ namespace MediaBrowser.Api.Images private ImageFormat[] GetClientSupportedFormats() { - //logger.LogDebug("Request types: {0}", string.Join(",", Request.AcceptTypes ?? Array.Empty())); - var supportedFormats = (Request.AcceptTypes ?? Array.Empty()).Select(i => i.Split(';')[0]).ToArray(); + var supportedFormats = Request.AcceptTypes ?? Array.Empty(); + if (supportedFormats.Length > 0) + { + for (int i = 0; i < supportedFormats.Length; i++) + { + int index = supportedFormats[i].IndexOf(';'); + if (index != -1) + { + supportedFormats[i] = supportedFormats[i].Substring(0, index); + } + } + } + var acceptParam = Request.QueryString["accept"]; var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); @@ -700,7 +709,7 @@ namespace MediaBrowser.Api.Images return formats.ToArray(); } - private bool SupportsFormat(string[] requestAcceptTypes, string acceptParam, string format, bool acceptAll) + private bool SupportsFormat(IEnumerable requestAcceptTypes, string acceptParam, string format, bool acceptAll) { var mimeType = "image/" + format; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 43fee79a10..39b6eedfeb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -36,10 +36,20 @@ namespace MediaBrowser.Controller.Entities /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo { - protected static MetadataFields[] EmptyMetadataFieldsArray = Array.Empty(); - protected static MediaUrl[] EmptyMediaUrlArray = Array.Empty(); - protected static ItemImageInfo[] EmptyItemImageInfoArray = Array.Empty(); - public static readonly LinkedChild[] EmptyLinkedChildArray = Array.Empty(); + private static readonly List _supportedExtensions = new List(SupportedImageExtensions) + { + ".nfo", + ".xml", + ".srt", + ".vtt", + ".sub", + ".idx", + ".txt", + ".edl", + ".bif", + ".smi", + ".ttml" + }; protected BaseItem() { @@ -49,8 +59,8 @@ namespace MediaBrowser.Controller.Entities Genres = Array.Empty(); Studios = Array.Empty(); ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - LockedFields = EmptyMetadataFieldsArray; - ImageInfos = EmptyItemImageInfoArray; + LockedFields = Array.Empty(); + ImageInfos = Array.Empty(); ProductionLocations = Array.Empty(); RemoteTrailers = Array.Empty(); ExtraIds = Array.Empty(); @@ -2452,10 +2462,8 @@ namespace MediaBrowser.Controller.Entities } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); - var extensions = new List { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; - extensions.AddRange(SupportedImageExtensions); - return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), extensions.ToArray(), false, false) + return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false) .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) .ToList(); } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e49ff20baa..c056bc0b4e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Controller.Entities public Folder() { - LinkedChildren = EmptyLinkedChildArray; + LinkedChildren = Array.Empty(); } [IgnoreDataMember] diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index ff6b133982..8484938642 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -25,22 +25,10 @@ namespace MediaBrowser.Controller.Entities public DateTime DateModified { get; set; } public int Width { get; set; } + public int Height { get; set; } [IgnoreDataMember] - public bool IsLocalFile - { - get - { - if (Path != null) - { - if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return true; - } - } + public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 124a943ef8..a532b5ee9e 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities.Movies { public BoxSet() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 232d116241..20c5b35219 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -21,10 +21,10 @@ namespace MediaBrowser.Controller.Entities.Movies public Movie() { - SpecialFeatureIds = new Guid[] { }; - RemoteTrailers = EmptyMediaUrlArray; - LocalTrailerIds = new Guid[] { }; - RemoteTrailerIds = new Guid[] { }; + SpecialFeatureIds = Array.Empty(); + RemoteTrailers = Array.Empty(); + LocalTrailerIds = Array.Empty(); + RemoteTrailerIds = Array.Empty(); } public Guid[] LocalTrailerIds { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 072b1d89af..fb29c07b0d 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities.TV { public Episode() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 570e9389e9..eae834f6f0 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; @@ -22,7 +21,7 @@ namespace MediaBrowser.Controller.Entities.TV { public Series() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); AirDays = Array.Empty(); diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 31cd429756..8379dcc090 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities AdditionalParts = Array.Empty(); LocalAlternateVersions = Array.Empty(); SubtitleFiles = Array.Empty(); - LinkedAlternateVersions = EmptyLinkedChildArray; + LinkedAlternateVersions = Array.Empty(); } public override bool CanDownload() diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index e0771245fd..ca99b28ca4 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; namespace MediaBrowser.Model.IO { @@ -177,7 +176,7 @@ namespace MediaBrowser.Model.IO /// IEnumerable GetFiles(string path, bool recursive = false); - IEnumerable GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive); + IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive); /// /// Gets the file system entries. From cedf50eeeca417a65d927261ae51c055b355db8c Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 21:29:17 +0100 Subject: [PATCH 0178/6208] Fix the 2 new warnings --- Jellyfin.Server/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a43c3fea5c..0ef1711d4c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -125,7 +125,7 @@ namespace Jellyfin.Server SQLitePCL.Batteries_V2.Init(); // Allow all https requests - ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } ); + ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths); @@ -144,7 +144,6 @@ namespace Jellyfin.Server appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); await appHost.RunStartupTasks().ConfigureAwait(false); - // TODO: read input for a stop command try { From 37ea50a572e8bd00aad39c193087228a5d3a3433 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 24 Feb 2019 15:47:59 +0100 Subject: [PATCH 0179/6208] Reduce the amount of exceptions thrown --- Emby.Dlna/PlayTo/Device.cs | 9 +- .../Diagnostics/CommonProcess.cs | 2 +- .../Library/LibraryManager.cs | 27 +- .../LiveTv/EmbyTV/EmbyTV.cs | 4 +- .../LiveTv/EmbyTV/ItemDataProvider.cs | 26 +- .../MediaEncoder/EncodingManager.cs | 4 + MediaBrowser.Api/ApiEntryPoint.cs | 5 + .../MediaEncoding/IMediaEncoder.cs | 2 +- .../InternalMetadataFolderImageProvider.cs | 16 +- .../Encoder/EncodingUtils.cs | 6 +- .../Encoder/MediaEncoder.cs | 68 +-- .../MediaInfo/SubtitleResolver.cs | 13 +- SocketHttpListener/Net/HttpConnection.cs | 532 ++++++++++++++++++ 13 files changed, 632 insertions(+), 82 deletions(-) create mode 100644 SocketHttpListener/Net/HttpConnection.cs diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index b62c5e1d4c..0c5ddee654 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1126,6 +1126,11 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackStart(uBaseObject mediaInfo) { + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) + { + return; + } + PlaybackStart?.Invoke(this, new PlaybackStartEventArgs { MediaInfo = mediaInfo @@ -1134,8 +1139,7 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackProgress(uBaseObject mediaInfo) { - var mediaUrl = mediaInfo.Url; - if (string.IsNullOrWhiteSpace(mediaUrl)) + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { return; } @@ -1148,7 +1152,6 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackStop(uBaseObject mediaInfo) { - PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs { MediaInfo = mediaInfo diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs index 78b22bda3f..2c4ef170dc 100644 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Diagnostics public void Dispose() { - _process.Dispose(); + _process?.Dispose(); } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 3c2272b566..6591d54c56 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -278,6 +278,7 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(item)); } + if (item is IItemByName) { if (!(item is MusicArtist)) @@ -285,18 +286,7 @@ namespace Emby.Server.Implementations.Library return; } } - - else if (item.IsFolder) - { - //if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder)) - //{ - // if (item.SourceType != SourceType.Library) - // { - // return; - // } - //} - } - else + else if (!item.IsFolder) { if (!(item is Video) && !(item is LiveTvChannel)) { @@ -371,19 +361,20 @@ namespace Emby.Server.Implementations.Library foreach (var metadataPath in GetMetadataPaths(item, children)) { - _logger.LogDebug("Deleting path {0}", metadataPath); + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug("Deleting path {MetadataPath}", metadataPath); try { Directory.Delete(metadataPath, true); - } - catch (IOException) - { - } catch (Exception ex) { - _logger.LogError(ex, "Error deleting {metadataPath}", metadataPath); + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index fceb82ba19..f424bdf5c4 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -105,8 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _mediaSourceManager = mediaSourceManager; _streamHelper = streamHelper; - _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); - _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger); + _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json")); + _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger); _timerProvider.TimerFired += _timerProvider_TimerFired; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index a2ac60b319..9c45ee36a2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -32,32 +31,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (_items == null) { + if (!File.Exists(_dataPath)) + { + return new List(); + } + Logger.LogInformation("Loading live tv data from {0}", _dataPath); _items = GetItemsFromFile(_dataPath); } + return _items.ToList(); } } private List GetItemsFromFile(string path) { - var jsonFile = path + ".json"; - - if (!File.Exists(jsonFile)) - { - return new List(); - } - try { - return _jsonSerializer.DeserializeFromFile>(jsonFile) ?? new List(); - } - catch (IOException) - { + return _jsonSerializer.DeserializeFromFile>(path); } catch (Exception ex) { - Logger.LogError(ex, "Error deserializing {jsonFile}", jsonFile); + Logger.LogError(ex, "Error deserializing {Path}", path); } return new List(); @@ -70,12 +65,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(newList)); } - var file = _dataPath + ".json"; - Directory.CreateDirectory(Path.GetDirectoryName(file)); + Directory.CreateDirectory(Path.GetDirectoryName(_dataPath)); lock (_fileDataLock) { - _jsonSerializer.SerializeToFile(newList, file); + _jsonSerializer.SerializeToFile(newList, _dataPath); _items = newList; } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index e68046f6d7..52d07d784d 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -202,6 +202,10 @@ namespace Emby.Server.Implementations.MediaEncoder private static List GetSavedChapterImages(Video video, IDirectoryService directoryService) { var path = GetChapterImagesPath(video); + if (!Directory.Exists(path)) + { + return new List(); + } try { diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index ceff6b02e5..700cbb9439 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -172,6 +172,11 @@ namespace MediaBrowser.Api { var path = _config.ApplicationPaths.GetTranscodingTempPath(); + if (!Directory.Exists(path)) + { + return; + } + foreach (var file in _fileSystem.GetFilePaths(path, true)) { _fileSystem.DeleteFile(file); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index d4ac3b7c37..d032a849e7 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The input files. /// The protocol. /// System.String. - string GetInputArgument(string[] inputFiles, MediaProtocol protocol); + string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol); /// /// Gets the time parameter. diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index c0706ceeb0..25a8ad5966 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Images { @@ -12,11 +13,16 @@ namespace MediaBrowser.LocalMetadata.Images { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; - public InternalMetadataFolderImageProvider(IServerConfigurationManager config, IFileSystem fileSystem) + public InternalMetadataFolderImageProvider( + IServerConfigurationManager config, + IFileSystem fileSystem, + ILogger logger) { _config = config; _fileSystem = fileSystem; + _logger = logger; } public string Name => "Internal Images"; @@ -53,12 +59,18 @@ namespace MediaBrowser.LocalMetadata.Images { var path = item.GetInternalMetadataPath(); + if (!Directory.Exists(path)) + { + return new List(); + } + try { return new LocalImageProvider(_fileSystem).GetImages(item, path, false, directoryService); } - catch (IOException) + catch (IOException ex) { + _logger.LogError(ex, "Error while getting images for {Library}", item.Name); return new List(); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 44e62446b2..d4aede572b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -6,11 +6,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { public static class EncodingUtils { - public static string GetInputArgument(List inputFiles, MediaProtocol protocol) + public static string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) { if (protocol != MediaProtocol.File) { - var url = inputFiles.First(); + var url = inputFiles[0]; return string.Format("\"{0}\"", url); } @@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // If there's more than one we'll need to use the concat command if (inputFiles.Count > 1) { - var files = string.Join("|", inputFiles.Select(NormalizePath).ToArray()); + var files = string.Join("|", inputFiles.Select(NormalizePath)); return string.Format("concat:\"{0}\"", files); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 2924577888..f6ff719d58 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -334,10 +334,8 @@ namespace MediaBrowser.MediaEncoding.Encoder /// The protocol. /// System.String. /// Unrecognized InputType - public string GetInputArgument(string[] inputFiles, MediaProtocol protocol) - { - return EncodingUtils.GetInputArgument(inputFiles.ToList(), protocol); - } + public string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) + => EncodingUtils.GetInputArgument(inputFiles, protocol); /// /// Gets the media info internal. @@ -354,8 +352,9 @@ namespace MediaBrowser.MediaEncoding.Encoder CancellationToken cancellationToken) { var args = extractChapters - ? "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_chapters -show_format" - : "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format"; + ? "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_chapters -show_format" + : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; + args = string.Format(args, probeSizeArgument, inputPath).Trim(); var process = _processFactory.Create(new ProcessOptions { @@ -364,8 +363,10 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, + FileName = FFprobePath, - Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), + Arguments = args, + IsHidden = true, ErrorDialog = false, @@ -383,36 +384,13 @@ namespace MediaBrowser.MediaEncoding.Encoder using (var processWrapper = new ProcessWrapper(process, this, _logger)) { + _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); + InternalMediaInfoResult result; try { - //process.BeginErrorReadLine(); - - var result = await _jsonSerializer.DeserializeFromStreamAsync(process.StandardOutput.BaseStream).ConfigureAwait(false); - - if (result == null || (result.streams == null && result.format == null)) - { - throw new Exception("ffprobe failed - streams and format are both null."); - } - - if (result.streams != null) - { - // Normalize aspect ratio if invalid - foreach (var stream in result.streams) - { - if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) - { - stream.display_aspect_ratio = string.Empty; - } - if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) - { - stream.sample_aspect_ratio = string.Empty; - } - } - } - - return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + result = await _jsonSerializer.DeserializeFromStreamAsync(process.StandardOutput.BaseStream).ConfigureAwait(false); } catch { @@ -420,6 +398,30 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } + + if (result == null || (result.streams == null && result.format == null)) + { + throw new Exception("ffprobe failed - streams and format are both null."); + } + + if (result.streams != null) + { + // Normalize aspect ratio if invalid + foreach (var stream in result.streams) + { + if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.display_aspect_ratio = string.Empty; + } + + if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.sample_aspect_ratio = string.Empty; + } + } + } + + return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index cd026b39b8..8195591e17 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; - private string[] SubtitleExtensions = new[] + private static readonly HashSet SubtitleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".srt", ".ssa", @@ -49,9 +49,16 @@ namespace MediaBrowser.Providers.MediaInfo startIndex += streams.Count; + string folder = video.GetInternalMetadataPath(); + + if (!Directory.Exists(folder)) + { + return streams; + } + try { - AddExternalSubtitleStreams(streams, video.GetInternalMetadataPath(), video.Path, startIndex, directoryService, clearCache); + AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache); } catch (IOException) { @@ -105,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo { var extension = Path.GetExtension(fullName); - if (!SubtitleExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!SubtitleExtensions.Contains(extension)) { continue; } diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs new file mode 100644 index 0000000000..5beea5f221 --- /dev/null +++ b/SocketHttpListener/Net/HttpConnection.cs @@ -0,0 +1,532 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.System; +using Microsoft.Extensions.Logging; +namespace SocketHttpListener.Net +{ + sealed class HttpConnection + { + private static AsyncCallback s_onreadCallback = new AsyncCallback(OnRead); + const int BufferSize = 8192; + Socket _socket; + Stream _stream; + HttpEndPointListener _epl; + MemoryStream _memoryStream; + byte[] _buffer; + HttpListenerContext _context; + StringBuilder _currentLine; + ListenerPrefix _prefix; + HttpRequestStream _requestStream; + HttpResponseStream _responseStream; + bool _chunked; + int _reuses; + bool _contextBound; + bool secure; + IPEndPoint local_ep; + HttpListener _lastListener; + X509Certificate cert; + SslStream ssl_stream; + + private readonly ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamHelper _streamHelper; + private readonly IFileSystem _fileSystem; + private readonly IEnvironmentInfo _environment; + + public HttpConnection(ILogger logger, Socket socket, HttpEndPointListener epl, bool secure, + X509Certificate cert, ICryptoProvider cryptoProvider, IStreamHelper streamHelper, IFileSystem fileSystem, + IEnvironmentInfo environment) + { + _logger = logger; + this._socket = socket; + this._epl = epl; + this.secure = secure; + this.cert = cert; + _cryptoProvider = cryptoProvider; + _streamHelper = streamHelper; + _fileSystem = fileSystem; + _environment = environment; + + if (secure == false) + { + _stream = new SocketStream(_socket, false); + } + else + { + ssl_stream = new SslStream(new SocketStream(_socket, false), false, (t, c, ch, e) => + { + if (c == null) + { + return true; + } + + //var c2 = c as X509Certificate2; + //if (c2 == null) + //{ + // c2 = new X509Certificate2(c.GetRawCertData()); + //} + + //_clientCert = c2; + //_clientCertErrors = new int[] { (int)e }; + return true; + }); + + _stream = ssl_stream; + } + } + + public Stream Stream => _stream; + + public async Task Init() + { + if (ssl_stream != null) + { + var enableAsync = true; + if (enableAsync) + { + await ssl_stream.AuthenticateAsServerAsync(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false).ConfigureAwait(false); + } + else + { + ssl_stream.AuthenticateAsServer(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false); + } + } + + InitInternal(); + } + + private void InitInternal() + { + _contextBound = false; + _requestStream = null; + _responseStream = null; + _prefix = null; + _chunked = false; + _memoryStream = new MemoryStream(); + _position = 0; + _inputState = InputState.RequestLine; + _lineState = LineState.None; + _context = new HttpListenerContext(this); + } + + public bool IsClosed => (_socket == null); + + public int Reuses => _reuses; + + public IPEndPoint LocalEndPoint + { + get + { + if (local_ep != null) + return local_ep; + + local_ep = (IPEndPoint)_socket.LocalEndPoint; + return local_ep; + } + } + + public IPEndPoint RemoteEndPoint => _socket.RemoteEndPoint as IPEndPoint; + + public bool IsSecure => secure; + + public ListenerPrefix Prefix + { + get => _prefix; + set => _prefix = value; + } + + private void OnTimeout(object unused) + { + //_logger.LogInformation("HttpConnection timer fired"); + CloseSocket(); + Unbind(); + } + + public void BeginReadRequest() + { + if (_buffer == null) + { + _buffer = new byte[BufferSize]; + } + + try + { + _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); + } + catch + { + CloseSocket(); + Unbind(); + } + } + + public HttpRequestStream GetRequestStream(bool chunked, long contentlength) + { + if (_requestStream == null) + { + byte[] buffer = _memoryStream.GetBuffer(); + int length = (int)_memoryStream.Length; + _memoryStream = null; + if (chunked) + { + _chunked = true; + //_context.Response.SendChunked = true; + _requestStream = new ChunkedInputStream(_context, _stream, buffer, _position, length - _position); + } + else + { + _requestStream = new HttpRequestStream(_stream, buffer, _position, length - _position, contentlength); + } + } + return _requestStream; + } + + public HttpResponseStream GetResponseStream(bool isExpect100Continue = false) + { + // TODO: can we get this _stream before reading the input? + if (_responseStream == null) + { + var supportsDirectSocketAccess = !_context.Response.SendChunked && !isExpect100Continue && !secure; + + _responseStream = new HttpResponseStream(_stream, _context.Response, false, _streamHelper, _socket, supportsDirectSocketAccess, _environment, _fileSystem, _logger); + } + return _responseStream; + } + + private static void OnRead(IAsyncResult ares) + { + var cnc = (HttpConnection)ares.AsyncState; + cnc.OnReadInternal(ares); + } + + private void OnReadInternal(IAsyncResult ares) + { + int nread = -1; + try + { + nread = _stream.EndRead(ares); + _memoryStream.Write(_buffer, 0, nread); + if (_memoryStream.Length > 32768) + { + SendError("Bad Request", 400); + Close(true); + return; + } + } + catch + { + if (_memoryStream != null && _memoryStream.Length > 0) + { + SendError(); + } + + if (_socket != null) + { + CloseSocket(); + Unbind(); + } + return; + } + + if (nread == 0) + { + CloseSocket(); + Unbind(); + return; + } + + if (ProcessInput(_memoryStream)) + { + if (!_context.HaveError) + _context.Request.FinishInitialization(); + + if (_context.HaveError) + { + SendError(); + Close(true); + return; + } + + if (!_epl.BindContext(_context)) + { + const int NotFoundErrorCode = 404; + SendError(HttpStatusDescription.Get(NotFoundErrorCode), NotFoundErrorCode); + Close(true); + return; + } + HttpListener listener = _epl.Listener; + if (_lastListener != listener) + { + RemoveConnection(); + listener.AddConnection(this); + _lastListener = listener; + } + + _contextBound = true; + listener.RegisterContext(_context); + return; + } + _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); + } + + private void RemoveConnection() + { + if (_lastListener == null) + _epl.RemoveConnection(this); + else + _lastListener.RemoveConnection(this); + } + + private enum InputState + { + RequestLine, + Headers + } + + private enum LineState + { + None, + CR, + LF + } + + InputState _inputState = InputState.RequestLine; + LineState _lineState = LineState.None; + int _position; + + // true -> done processing + // false -> need more input + private bool ProcessInput(MemoryStream ms) + { + byte[] buffer = ms.GetBuffer(); + int len = (int)ms.Length; + int used = 0; + string line; + + while (true) + { + if (_context.HaveError) + return true; + + if (_position >= len) + break; + + try + { + line = ReadLine(buffer, _position, len - _position, ref used); + _position += used; + } + catch + { + _context.ErrorMessage = "Bad request"; + _context.ErrorStatus = 400; + return true; + } + + if (line == null) + break; + + if (line == "") + { + if (_inputState == InputState.RequestLine) + continue; + _currentLine = null; + ms = null; + return true; + } + + if (_inputState == InputState.RequestLine) + { + _context.Request.SetRequestLine(line); + _inputState = InputState.Headers; + } + else + { + try + { + _context.Request.AddHeader(line); + } + catch (Exception e) + { + _context.ErrorMessage = e.Message; + _context.ErrorStatus = 400; + return true; + } + } + } + + if (used == len) + { + ms.SetLength(0); + _position = 0; + } + return false; + } + + private string ReadLine(byte[] buffer, int offset, int len, ref int used) + { + if (_currentLine == null) + _currentLine = new StringBuilder(128); + int last = offset + len; + used = 0; + for (int i = offset; i < last && _lineState != LineState.LF; i++) + { + used++; + byte b = buffer[i]; + if (b == 13) + { + _lineState = LineState.CR; + } + else if (b == 10) + { + _lineState = LineState.LF; + } + else + { + _currentLine.Append((char)b); + } + } + + string result = null; + if (_lineState == LineState.LF) + { + _lineState = LineState.None; + result = _currentLine.ToString(); + _currentLine.Length = 0; + } + + return result; + } + + public void SendError(string msg, int status) + { + try + { + HttpListenerResponse response = _context.Response; + response.StatusCode = status; + response.ContentType = "text/html"; + string description = HttpStatusDescription.Get(status); + string str; + if (msg != null) + str = string.Format("

{0} ({1})

", description, msg); + else + str = string.Format("

{0}

", description); + + byte[] error = Encoding.UTF8.GetBytes(str); + response.Close(error, false); + } + catch + { + // response was already closed + } + } + + public void SendError() + { + SendError(_context.ErrorMessage, _context.ErrorStatus); + } + + private void Unbind() + { + if (_contextBound) + { + _epl.UnbindContext(_context); + _contextBound = false; + } + } + + public void Close() + { + Close(false); + } + + private void CloseSocket() + { + if (_socket == null) + return; + + try + { + _socket.Close(); + } + catch { } + finally + { + _socket = null; + } + + RemoveConnection(); + } + + internal void Close(bool force) + { + if (_socket != null) + { + Stream st = GetResponseStream(); + if (st != null) + st.Close(); + + _responseStream = null; + } + + if (_socket != null) + { + force |= !_context.Request.KeepAlive; + if (!force) + { + force = string.Equals(_context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase); + } + + if (!force && _context.Request.FlushInput()) + { + if (_chunked && _context.Response.ForceCloseChunked == false) + { + // Don't close. Keep working. + _reuses++; + Unbind(); + InitInternal(); + BeginReadRequest(); + return; + } + + _reuses++; + Unbind(); + InitInternal(); + BeginReadRequest(); + return; + } + + Socket s = _socket; + _socket = null; + try + { + s?.Shutdown(SocketShutdown.Both); + } + catch + { + } + finally + { + try + { + s?.Close(); + } + catch { } + } + Unbind(); + RemoveConnection(); + return; + } + } + } +} From ab9859ecefa7ec28b38cc807226b3df31fdf3e93 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 13:12:37 +0100 Subject: [PATCH 0180/6208] Address comment --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index f6ff719d58..94beda3dbb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -390,7 +390,8 @@ namespace MediaBrowser.MediaEncoding.Encoder InternalMediaInfoResult result; try { - result = await _jsonSerializer.DeserializeFromStreamAsync(process.StandardOutput.BaseStream).ConfigureAwait(false); + result = await _jsonSerializer.DeserializeFromStreamAsync( + process.StandardOutput.BaseStream).ConfigureAwait(false); } catch { From 620d7b560d0b72f532d96a5b459f14fd4ad3044a Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 28 Feb 2019 22:33:20 +0100 Subject: [PATCH 0181/6208] Fail on warnings for Jellyfin.Server --- .../Activity/ActivityLogEntryPoint.cs | 181 +++++++----------- .../Emby.Server.Implementations.csproj | 15 ++ .../SocketSharp/HttpPostedFile.cs | 44 ++--- Jellyfin.Server/Jellyfin.Server.csproj | 7 +- jellyfin.ruleset | 5 + 5 files changed, 118 insertions(+), 134 deletions(-) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 739f687678..98cd97c318 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -30,13 +30,10 @@ namespace Emby.Server.Implementations.Activity public class ActivityLogEntryPoint : IServerEntryPoint { private readonly IInstallationManager _installationManager; - - //private readonly ILogger _logger; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; @@ -61,41 +58,37 @@ namespace Emby.Server.Implementations.Activity public Task RunAsync() { - _taskManager.TaskCompleted += _taskManager_TaskCompleted; + _taskManager.TaskCompleted += OnTaskCompleted; - _installationManager.PluginInstalled += _installationManager_PluginInstalled; - _installationManager.PluginUninstalled += _installationManager_PluginUninstalled; - _installationManager.PluginUpdated += _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; + _installationManager.PluginInstalled += OnPluginInstalled; + _installationManager.PluginUninstalled += OnPluginUninstalled; + _installationManager.PluginUpdated += OnPluginUpdated; + _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - _sessionManager.SessionStarted += _sessionManager_SessionStarted; - _sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed; - _sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded; - _sessionManager.SessionEnded += _sessionManager_SessionEnded; + _sessionManager.SessionStarted += OnSessionStarted; + _sessionManager.AuthenticationFailed += OnAuthenticationFailed; + _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded; + _sessionManager.SessionEnded += OnSessionEnded; - _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; + _sessionManager.PlaybackStart += OnPlaybackStart; + _sessionManager.PlaybackStopped += OnPlaybackStopped; - //_subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded; - _subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure; + _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; - _userManager.UserCreated += _userManager_UserCreated; - _userManager.UserPasswordChanged += _userManager_UserPasswordChanged; - _userManager.UserDeleted += _userManager_UserDeleted; - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - _userManager.UserLockedOut += _userManager_UserLockedOut; + _userManager.UserCreated += OnUserCreated; + _userManager.UserPasswordChanged += OnUserPasswordChanged; + _userManager.UserDeleted += OnUserDeleted; + _userManager.UserPolicyUpdated += OnUserPolicyUpdated; + _userManager.UserLockedOut += OnUserLockedOut; - //_config.ConfigurationUpdated += _config_ConfigurationUpdated; - //_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - _deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded; - - _appHost.ApplicationUpdated += _appHost_ApplicationUpdated; + _appHost.ApplicationUpdated += OnApplicationUpdated; return Task.CompletedTask; } - void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs e) + private void OnCameraImageUploaded(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -104,7 +97,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserLockedOut(object sender, GenericEventArgs e) + private void OnUserLockedOut(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -114,7 +107,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -125,7 +118,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) + private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -146,7 +139,7 @@ namespace Emby.Server.Implementations.Activity return; } - var user = e.Users.First(); + var user = e.Users[0]; CreateLogEntry(new ActivityLogEntry { @@ -156,7 +149,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) + private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -232,7 +225,7 @@ namespace Emby.Server.Implementations.Activity return null; } - void _sessionManager_SessionEnded(object sender, SessionEventArgs e) + private void OnSessionEnded(object sender, SessionEventArgs e) { string name; var session = e.SessionInfo; @@ -258,7 +251,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs e) + private void OnAuthenticationSucceeded(object sender, GenericEventArgs e) { var user = e.Argument.User; @@ -271,7 +264,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_AuthenticationFailed(object sender, GenericEventArgs e) + private void OnAuthenticationFailed(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -282,7 +275,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _appHost_ApplicationUpdated(object sender, GenericEventArgs e) + private void OnApplicationUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -292,25 +285,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("MessageNamedServerConfigurationUpdatedWithValue"), e.Key), - Type = "NamedConfigurationUpdated" - }); - } - - void _config_ConfigurationUpdated(object sender, EventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = _localization.GetLocalizedString("MessageServerConfigurationUpdated"), - Type = "ServerConfigurationUpdated" - }); - } - - void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + private void OnUserPolicyUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -320,7 +295,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserDeleted(object sender, GenericEventArgs e) + private void OnUserDeleted(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -329,7 +304,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserPasswordChanged(object sender, GenericEventArgs e) + private void OnUserPasswordChanged(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -339,7 +314,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserCreated(object sender, GenericEventArgs e) + private void OnUserCreated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -349,18 +324,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("SubtitlesDownloadedForItem"), Notifications.Notifications.GetItemName(e.Item)), - Type = "SubtitlesDownloaded", - ItemId = e.Item.Id.ToString("N"), - ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider) - }); - } - - void _sessionManager_SessionStarted(object sender, SessionEventArgs e) + private void OnSessionStarted(object sender, SessionEventArgs e) { string name; var session = e.SessionInfo; @@ -386,7 +350,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginUpdated(object sender, GenericEventArgs> e) + private void OnPluginUpdated(object sender, GenericEventArgs> e) { CreateLogEntry(new ActivityLogEntry { @@ -397,7 +361,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginUninstalled(object sender, GenericEventArgs e) + private void OnPluginUninstalled(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -406,7 +370,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginInstalled(object sender, GenericEventArgs e) + private void OnPluginInstalled(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -416,7 +380,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; @@ -429,7 +393,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) + private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; @@ -468,48 +432,36 @@ namespace Emby.Server.Implementations.Activity } private void CreateLogEntry(ActivityLogEntry entry) - { - try - { - _activityManager.Create(entry); - } - catch - { - // Logged at lower levels - } - } + => _activityManager.Create(entry); public void Dispose() { - _taskManager.TaskCompleted -= _taskManager_TaskCompleted; + _taskManager.TaskCompleted -= OnTaskCompleted; - _installationManager.PluginInstalled -= _installationManager_PluginInstalled; - _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; - _installationManager.PluginUpdated -= _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; + _installationManager.PluginInstalled -= OnPluginInstalled; + _installationManager.PluginUninstalled -= OnPluginUninstalled; + _installationManager.PluginUpdated -= OnPluginUpdated; + _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - _sessionManager.SessionStarted -= _sessionManager_SessionStarted; - _sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed; - _sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded; - _sessionManager.SessionEnded -= _sessionManager_SessionEnded; + _sessionManager.SessionStarted -= OnSessionStarted; + _sessionManager.AuthenticationFailed -= OnAuthenticationFailed; + _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded; + _sessionManager.SessionEnded -= OnSessionEnded; - _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; + _sessionManager.PlaybackStart -= OnPlaybackStart; + _sessionManager.PlaybackStopped -= OnPlaybackStopped; - _subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure; + _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; - _userManager.UserCreated -= _userManager_UserCreated; - _userManager.UserPasswordChanged -= _userManager_UserPasswordChanged; - _userManager.UserDeleted -= _userManager_UserDeleted; - _userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated; - _userManager.UserLockedOut -= _userManager_UserLockedOut; + _userManager.UserCreated -= OnUserCreated; + _userManager.UserPasswordChanged -= OnUserPasswordChanged; + _userManager.UserDeleted -= OnUserDeleted; + _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; + _userManager.UserLockedOut -= OnUserLockedOut; - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; - _config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated; + _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; - _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded; - - _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated; + _appHost.ApplicationUpdated -= OnApplicationUpdated; } /// @@ -531,6 +483,7 @@ namespace Emby.Server.Implementations.Activity values.Add(CreateValueString(years, "year")); days = days % DaysInYear; } + // Number of months if (days >= DaysInMonth) { @@ -538,25 +491,39 @@ namespace Emby.Server.Implementations.Activity values.Add(CreateValueString(months, "month")); days = days % DaysInMonth; } + // Number of days if (days >= 1) + { values.Add(CreateValueString(days, "day")); + } + // Number of hours if (span.Hours >= 1) + { values.Add(CreateValueString(span.Hours, "hour")); + } // Number of minutes if (span.Minutes >= 1) + { values.Add(CreateValueString(span.Minutes, "minute")); + } + // Number of seconds (include when 0 if no other components included) if (span.Seconds >= 1 || values.Count == 0) + { values.Add(CreateValueString(span.Seconds, "second")); + } // Combine values into string var builder = new StringBuilder(); for (int i = 0; i < values.Count; i++) { if (builder.Length > 0) + { builder.Append(i == values.Count - 1 ? " and " : ", "); + } + builder.Append(values[i]); } // Return result diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 39e9ed3756..3cbd2792d9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -47,6 +47,21 @@ false + + true + + + + + + + + + + + ../jellyfin.ruleset + + diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs index f38ed848ee..95b7912fbb 100644 --- a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs +++ b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs @@ -63,6 +63,28 @@ public sealed class HttpPostedFile : IDisposable _position = offset; } + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _end - _offset; + + public override long Position + { + get => _position - _offset; + set + { + if (value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _position = Seek(value, SeekOrigin.Begin); + } + } + public override void Flush() { } @@ -178,27 +200,5 @@ public sealed class HttpPostedFile : IDisposable { throw new NotSupportedException(); } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => _end - _offset; - - public override long Position - { - get => _position - _offset; - set - { - if (value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _position = Seek(value, SeekOrigin.Begin); - } - } } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bd670df527..9346a2d254 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -12,7 +12,8 @@ latest - SA1600;CS1591 + SA1600;SA1601;CS1591 + true @@ -23,10 +24,6 @@ - - true - - diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 4381349ca6..0a04b4c557 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -1,6 +1,11 @@ + + + + + From 8a680ae324fbc97280396d0f49a60fcdd05f36f6 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Thu, 7 Mar 2019 22:00:28 +0100 Subject: [PATCH 0182/6208] Update to renamed DownloadGitHubRelease task (Microsoft/azure-pipelines-tasks#9481) --- .ci/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index e5845c0efa..4cd19dc838 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -156,7 +156,7 @@ jobs: overWrite: true # Optional flattenFolders: true # Optional - - task: DownloadGitHubReleases@0 + - task: DownloadGitHubRelease@0 displayName: Download ABI compatibility check tool from GitHub inputs: connection: Jellyfin GitHub From d014a1dc1d6e2c9f2cd34dff768a94521f2a0cb9 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 22:16:27 +0100 Subject: [PATCH 0183/6208] Remove file added in #996 --- SocketHttpListener/Net/HttpConnection.cs | 532 ----------------------- 1 file changed, 532 deletions(-) delete mode 100644 SocketHttpListener/Net/HttpConnection.cs diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs deleted file mode 100644 index 5beea5f221..0000000000 --- a/SocketHttpListener/Net/HttpConnection.cs +++ /dev/null @@ -1,532 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; -namespace SocketHttpListener.Net -{ - sealed class HttpConnection - { - private static AsyncCallback s_onreadCallback = new AsyncCallback(OnRead); - const int BufferSize = 8192; - Socket _socket; - Stream _stream; - HttpEndPointListener _epl; - MemoryStream _memoryStream; - byte[] _buffer; - HttpListenerContext _context; - StringBuilder _currentLine; - ListenerPrefix _prefix; - HttpRequestStream _requestStream; - HttpResponseStream _responseStream; - bool _chunked; - int _reuses; - bool _contextBound; - bool secure; - IPEndPoint local_ep; - HttpListener _lastListener; - X509Certificate cert; - SslStream ssl_stream; - - private readonly ILogger _logger; - private readonly ICryptoProvider _cryptoProvider; - private readonly IStreamHelper _streamHelper; - private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environment; - - public HttpConnection(ILogger logger, Socket socket, HttpEndPointListener epl, bool secure, - X509Certificate cert, ICryptoProvider cryptoProvider, IStreamHelper streamHelper, IFileSystem fileSystem, - IEnvironmentInfo environment) - { - _logger = logger; - this._socket = socket; - this._epl = epl; - this.secure = secure; - this.cert = cert; - _cryptoProvider = cryptoProvider; - _streamHelper = streamHelper; - _fileSystem = fileSystem; - _environment = environment; - - if (secure == false) - { - _stream = new SocketStream(_socket, false); - } - else - { - ssl_stream = new SslStream(new SocketStream(_socket, false), false, (t, c, ch, e) => - { - if (c == null) - { - return true; - } - - //var c2 = c as X509Certificate2; - //if (c2 == null) - //{ - // c2 = new X509Certificate2(c.GetRawCertData()); - //} - - //_clientCert = c2; - //_clientCertErrors = new int[] { (int)e }; - return true; - }); - - _stream = ssl_stream; - } - } - - public Stream Stream => _stream; - - public async Task Init() - { - if (ssl_stream != null) - { - var enableAsync = true; - if (enableAsync) - { - await ssl_stream.AuthenticateAsServerAsync(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false).ConfigureAwait(false); - } - else - { - ssl_stream.AuthenticateAsServer(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false); - } - } - - InitInternal(); - } - - private void InitInternal() - { - _contextBound = false; - _requestStream = null; - _responseStream = null; - _prefix = null; - _chunked = false; - _memoryStream = new MemoryStream(); - _position = 0; - _inputState = InputState.RequestLine; - _lineState = LineState.None; - _context = new HttpListenerContext(this); - } - - public bool IsClosed => (_socket == null); - - public int Reuses => _reuses; - - public IPEndPoint LocalEndPoint - { - get - { - if (local_ep != null) - return local_ep; - - local_ep = (IPEndPoint)_socket.LocalEndPoint; - return local_ep; - } - } - - public IPEndPoint RemoteEndPoint => _socket.RemoteEndPoint as IPEndPoint; - - public bool IsSecure => secure; - - public ListenerPrefix Prefix - { - get => _prefix; - set => _prefix = value; - } - - private void OnTimeout(object unused) - { - //_logger.LogInformation("HttpConnection timer fired"); - CloseSocket(); - Unbind(); - } - - public void BeginReadRequest() - { - if (_buffer == null) - { - _buffer = new byte[BufferSize]; - } - - try - { - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - catch - { - CloseSocket(); - Unbind(); - } - } - - public HttpRequestStream GetRequestStream(bool chunked, long contentlength) - { - if (_requestStream == null) - { - byte[] buffer = _memoryStream.GetBuffer(); - int length = (int)_memoryStream.Length; - _memoryStream = null; - if (chunked) - { - _chunked = true; - //_context.Response.SendChunked = true; - _requestStream = new ChunkedInputStream(_context, _stream, buffer, _position, length - _position); - } - else - { - _requestStream = new HttpRequestStream(_stream, buffer, _position, length - _position, contentlength); - } - } - return _requestStream; - } - - public HttpResponseStream GetResponseStream(bool isExpect100Continue = false) - { - // TODO: can we get this _stream before reading the input? - if (_responseStream == null) - { - var supportsDirectSocketAccess = !_context.Response.SendChunked && !isExpect100Continue && !secure; - - _responseStream = new HttpResponseStream(_stream, _context.Response, false, _streamHelper, _socket, supportsDirectSocketAccess, _environment, _fileSystem, _logger); - } - return _responseStream; - } - - private static void OnRead(IAsyncResult ares) - { - var cnc = (HttpConnection)ares.AsyncState; - cnc.OnReadInternal(ares); - } - - private void OnReadInternal(IAsyncResult ares) - { - int nread = -1; - try - { - nread = _stream.EndRead(ares); - _memoryStream.Write(_buffer, 0, nread); - if (_memoryStream.Length > 32768) - { - SendError("Bad Request", 400); - Close(true); - return; - } - } - catch - { - if (_memoryStream != null && _memoryStream.Length > 0) - { - SendError(); - } - - if (_socket != null) - { - CloseSocket(); - Unbind(); - } - return; - } - - if (nread == 0) - { - CloseSocket(); - Unbind(); - return; - } - - if (ProcessInput(_memoryStream)) - { - if (!_context.HaveError) - _context.Request.FinishInitialization(); - - if (_context.HaveError) - { - SendError(); - Close(true); - return; - } - - if (!_epl.BindContext(_context)) - { - const int NotFoundErrorCode = 404; - SendError(HttpStatusDescription.Get(NotFoundErrorCode), NotFoundErrorCode); - Close(true); - return; - } - HttpListener listener = _epl.Listener; - if (_lastListener != listener) - { - RemoveConnection(); - listener.AddConnection(this); - _lastListener = listener; - } - - _contextBound = true; - listener.RegisterContext(_context); - return; - } - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - - private void RemoveConnection() - { - if (_lastListener == null) - _epl.RemoveConnection(this); - else - _lastListener.RemoveConnection(this); - } - - private enum InputState - { - RequestLine, - Headers - } - - private enum LineState - { - None, - CR, - LF - } - - InputState _inputState = InputState.RequestLine; - LineState _lineState = LineState.None; - int _position; - - // true -> done processing - // false -> need more input - private bool ProcessInput(MemoryStream ms) - { - byte[] buffer = ms.GetBuffer(); - int len = (int)ms.Length; - int used = 0; - string line; - - while (true) - { - if (_context.HaveError) - return true; - - if (_position >= len) - break; - - try - { - line = ReadLine(buffer, _position, len - _position, ref used); - _position += used; - } - catch - { - _context.ErrorMessage = "Bad request"; - _context.ErrorStatus = 400; - return true; - } - - if (line == null) - break; - - if (line == "") - { - if (_inputState == InputState.RequestLine) - continue; - _currentLine = null; - ms = null; - return true; - } - - if (_inputState == InputState.RequestLine) - { - _context.Request.SetRequestLine(line); - _inputState = InputState.Headers; - } - else - { - try - { - _context.Request.AddHeader(line); - } - catch (Exception e) - { - _context.ErrorMessage = e.Message; - _context.ErrorStatus = 400; - return true; - } - } - } - - if (used == len) - { - ms.SetLength(0); - _position = 0; - } - return false; - } - - private string ReadLine(byte[] buffer, int offset, int len, ref int used) - { - if (_currentLine == null) - _currentLine = new StringBuilder(128); - int last = offset + len; - used = 0; - for (int i = offset; i < last && _lineState != LineState.LF; i++) - { - used++; - byte b = buffer[i]; - if (b == 13) - { - _lineState = LineState.CR; - } - else if (b == 10) - { - _lineState = LineState.LF; - } - else - { - _currentLine.Append((char)b); - } - } - - string result = null; - if (_lineState == LineState.LF) - { - _lineState = LineState.None; - result = _currentLine.ToString(); - _currentLine.Length = 0; - } - - return result; - } - - public void SendError(string msg, int status) - { - try - { - HttpListenerResponse response = _context.Response; - response.StatusCode = status; - response.ContentType = "text/html"; - string description = HttpStatusDescription.Get(status); - string str; - if (msg != null) - str = string.Format("

{0} ({1})

", description, msg); - else - str = string.Format("

{0}

", description); - - byte[] error = Encoding.UTF8.GetBytes(str); - response.Close(error, false); - } - catch - { - // response was already closed - } - } - - public void SendError() - { - SendError(_context.ErrorMessage, _context.ErrorStatus); - } - - private void Unbind() - { - if (_contextBound) - { - _epl.UnbindContext(_context); - _contextBound = false; - } - } - - public void Close() - { - Close(false); - } - - private void CloseSocket() - { - if (_socket == null) - return; - - try - { - _socket.Close(); - } - catch { } - finally - { - _socket = null; - } - - RemoveConnection(); - } - - internal void Close(bool force) - { - if (_socket != null) - { - Stream st = GetResponseStream(); - if (st != null) - st.Close(); - - _responseStream = null; - } - - if (_socket != null) - { - force |= !_context.Request.KeepAlive; - if (!force) - { - force = string.Equals(_context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase); - } - - if (!force && _context.Request.FlushInput()) - { - if (_chunked && _context.Response.ForceCloseChunked == false) - { - // Don't close. Keep working. - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - Socket s = _socket; - _socket = null; - try - { - s?.Shutdown(SocketShutdown.Both); - } - catch - { - } - finally - { - try - { - s?.Close(); - } - catch { } - } - Unbind(); - RemoveConnection(); - return; - } - } - } -} From 3fa43a1e08a719e65ed38a57b556be0c0edacaef Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 7 Mar 2019 22:26:23 +0100 Subject: [PATCH 0184/6208] Don't set status code if response is closed --- .../HttpServer/HttpListenerHost.cs | 4 +- .../Services/ServiceExec.cs | 2 +- .../SocketSharp/WebSocketSharpListener.cs | 5 ++- .../SocketSharp/WebSocketSharpRequest.cs | 7 +--- .../SocketSharp/WebSocketSharpResponse.cs | 37 +++++-------------- MediaBrowser.Model/Services/IRequest.cs | 14 +------ 6 files changed, 19 insertions(+), 50 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index d1b1b5b242..eab755cefb 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.HttpServer var httpRes = httpReq.Response; - if (httpRes.IsClosed) + if (httpRes.OriginalResponse.HasStarted) { return; } @@ -595,8 +595,6 @@ namespace Emby.Server.Implementations.HttpServer } finally { - // TODO response closes automatically after the handler is done, but some functions rely on knowing if it's closed or not - httpRes.IsClosed = true; stopWatch.Stop(); var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index 79f5c59e65..38952628d8 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Services foreach (var requestFilter in actionContext.RequestFilters) { requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.IsClosed) + if (request.Response.OriginalResponse.HasStarted) { Task.FromResult(null); } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index 2df826957a..dd313b3363 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -90,7 +90,10 @@ namespace Emby.Server.Implementations.SocketSharp catch (Exception ex) { _logger.LogError(ex, "AcceptWebSocketAsync error"); - ctx.Response.StatusCode = 500; + if (!ctx.Response.HasStarted) + { + ctx.Response.StatusCode = 500; + } } } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index bc002dc4ce..2d3ec3c8ea 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -20,22 +20,19 @@ namespace Emby.Server.Implementations.SocketSharp public partial class WebSocketSharpRequest : IHttpRequest { private readonly HttpRequest request; - private readonly IResponse response; public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger) { this.OperationName = operationName; this.request = httpContext; - this.response = new WebSocketSharpResponse(logger, response, this); + this.Response = new WebSocketSharpResponse(logger, response); // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); } public HttpRequest HttpRequest => request; - public IResponse Response => response; - - public IResponse HttpResponse => response; + public IResponse Response { get; } public string OperationName { get; set; } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs index a7e3e6c700..0f67eaa622 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -16,38 +16,28 @@ namespace Emby.Server.Implementations.SocketSharp { private readonly ILogger _logger; - private readonly HttpResponse _response; - - public WebSocketSharpResponse(ILogger logger, HttpResponse response, IRequest request) + public WebSocketSharpResponse(ILogger logger, HttpResponse response) { _logger = logger; - this._response = response; - Items = new Dictionary(); - Request = request; + OriginalResponse = response; } - public IRequest Request { get; private set; } - - public Dictionary Items { get; private set; } - - public object OriginalResponse => _response; + public HttpResponse OriginalResponse { get; } public int StatusCode { - get => this._response.StatusCode; - set => this._response.StatusCode = value; + get => OriginalResponse.StatusCode; + set => OriginalResponse.StatusCode = value; } public string StatusDescription { get; set; } public string ContentType { - get => _response.ContentType; - set => _response.ContentType = value; + get => OriginalResponse.ContentType; + set => OriginalResponse.ContentType = value; } - public IHeaderDictionary Headers => _response.Headers; - public void AddHeader(string name, string value) { if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) @@ -56,22 +46,15 @@ namespace Emby.Server.Implementations.SocketSharp return; } - _response.Headers.Add(name, value); - } - - public string GetHeader(string name) - { - return _response.Headers[name]; + OriginalResponse.Headers.Add(name, value); } public void Redirect(string url) { - _response.Redirect(url); + OriginalResponse.Redirect(url); } - public Stream OutputStream => _response.Body; - - public bool IsClosed { get; set; } + public Stream OutputStream => OriginalResponse.Body; public bool SendChunked { get; set; } diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index edb5a2509b..4f6ddb476e 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -101,7 +101,7 @@ namespace MediaBrowser.Model.Services public interface IResponse { - IRequest Request { get; } + HttpResponse OriginalResponse { get; } int StatusCode { get; set; } @@ -111,22 +111,10 @@ namespace MediaBrowser.Model.Services void AddHeader(string name, string value); - string GetHeader(string name); - void Redirect(string url); Stream OutputStream { get; } - /// - /// Gets a value indicating whether this instance is closed. - /// - bool IsClosed { get; set; } - - //Add Metadata to Response - Dictionary Items { get; } - - IHeaderDictionary Headers { get; } - Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken); bool SendChunked { get; set; } From decaffed86cbc5db99c3f79d266fdfcdd262c12d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 17:39:40 +0100 Subject: [PATCH 0185/6208] Remove EnvironmentInfo This moved the last bit of usefulness of EnvironmentInfo into a static class. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 13 ++-- .../IsoMounter/LinuxIsoManager.cs | 10 ++- .../ApplicationHost.cs | 47 ++++-------- .../EnvironmentInfo/EnvironmentInfo.cs | 36 ---------- .../IO/LibraryMonitor.cs | 15 +--- .../IO/ManagedFileSystem.cs | 16 ++--- .../Networking/NetworkManager.cs | 8 +-- Jellyfin.Server/CoreAppHost.cs | 3 - Jellyfin.Server/Program.cs | 51 ++----------- MediaBrowser.Api/LiveTv/LiveTvService.cs | 8 +-- .../LiveTv/ProgressiveFileCopier.cs | 8 +-- .../Playback/Progressive/AudioService.cs | 8 +-- .../BaseProgressiveStreamingService.cs | 15 ++-- .../Progressive/ProgressiveStreamWriter.cs | 10 ++- .../Playback/Progressive/VideoService.cs | 8 +-- .../Playback/UniversalAudioService.cs | 7 +- MediaBrowser.Common/IApplicationHost.cs | 3 +- MediaBrowser.Common/System/OperatingSystem.cs | 72 +++++++++++++++++++ MediaBrowser.Model/System/IEnvironmentInfo.cs | 21 ------ .../System/OperatingSystemId.cs | 10 +++ 20 files changed, 144 insertions(+), 225 deletions(-) delete mode 100644 Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs create mode 100644 MediaBrowser.Common/System/OperatingSystem.cs delete mode 100644 MediaBrowser.Model/System/IEnvironmentInfo.cs create mode 100644 MediaBrowser.Model/System/OperatingSystemId.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 57ed0097a0..919a8f46f8 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; @@ -24,6 +23,7 @@ using MediaBrowser.Model.Xml; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Dlna.Main { @@ -48,9 +48,8 @@ namespace Emby.Dlna.Main private readonly IDeviceDiscovery _deviceDiscovery; private SsdpDevicePublisher _Publisher; - + private readonly ISocketFactory _socketFactory; - private readonly IEnvironmentInfo _environmentInfo; private readonly INetworkManager _networkManager; private ISsdpCommunicationsServer _communicationsServer; @@ -76,7 +75,6 @@ namespace Emby.Dlna.Main IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, - IEnvironmentInfo environmentInfo, INetworkManager networkManager, IUserViewManager userViewManager, IXmlReaderSettingsFactory xmlReaderSettingsFactory, @@ -96,7 +94,6 @@ namespace Emby.Dlna.Main _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; _socketFactory = socketFactory; - _environmentInfo = environmentInfo; _networkManager = networkManager; _logger = loggerFactory.CreateLogger("Dlna"); @@ -169,8 +166,8 @@ namespace Emby.Dlna.Main { if (_communicationsServer == null) { - var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows || - _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux; + var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || + OperatingSystem.Id == OperatingSystemId.Linux; _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) { @@ -230,7 +227,7 @@ namespace Emby.Dlna.Main try { - _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost); + _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _Publisher.LogFunction = LogMessage; _Publisher.SupportPnpRootDevice = false; diff --git a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs b/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs index 943caa3e62..2f0003be88 100644 --- a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs +++ b/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs @@ -7,6 +7,7 @@ using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace IsoMounter { @@ -17,7 +18,6 @@ namespace IsoMounter #region Private Fields - private readonly IEnvironmentInfo EnvironmentInfo; private readonly bool ExecutablesAvailable; private readonly ILogger _logger; private readonly string MountCommand; @@ -30,10 +30,8 @@ namespace IsoMounter #region Constructor(s) - public LinuxIsoManager(ILogger logger, IEnvironmentInfo environment, IProcessFactory processFactory) + public LinuxIsoManager(ILogger logger, IProcessFactory processFactory) { - - EnvironmentInfo = environment; _logger = logger; ProcessFactory = processFactory; @@ -109,7 +107,7 @@ namespace IsoMounter public bool CanMount(string path) { - if (EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Linux) + if (OperatingSystem.Id != OperatingSystemId.Linux) { return false; } @@ -118,7 +116,7 @@ namespace IsoMounter Name, path, Path.GetExtension(path), - EnvironmentInfo.OperatingSystem, + OperatingSystem.Name, ExecutablesAvailable ); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a581214c71..964d173421 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -115,6 +115,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using ServiceStack; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations { @@ -143,12 +144,8 @@ namespace Emby.Server.Implementations return false; } - if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) - { - return true; - } - - if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) + if (OperatingSystem.Id == OperatingSystemId.Windows + || OperatingSystem.Id == OperatingSystemId.Darwin) { return true; } @@ -218,8 +215,6 @@ namespace Emby.Server.Implementations public IFileSystem FileSystemManager { get; set; } - protected IEnvironmentInfo EnvironmentInfo { get; set; } - public PackageVersionClass SystemUpdateLevel { get @@ -239,15 +234,6 @@ namespace Emby.Server.Implementations /// The server configuration manager. public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; - /// - /// Gets the configuration manager. - /// - /// IConfigurationManager. - protected IConfigurationManager GetConfigurationManager() - { - return new ServerConfigurationManager(ApplicationPaths, LoggerFactory, XmlSerializer, FileSystemManager); - } - protected virtual IResourceFileManager CreateResourceFileManager() { return new ResourceFileManager(HttpResultFactory, LoggerFactory, FileSystemManager); @@ -356,7 +342,6 @@ namespace Emby.Server.Implementations ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - IEnvironmentInfo environmentInfo, IImageEncoder imageEncoder, INetworkManager networkManager, IConfiguration configuration) @@ -370,13 +355,12 @@ namespace Emby.Server.Implementations NetworkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; - EnvironmentInfo = environmentInfo; ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; FileSystemManager = fileSystem; - ConfigurationManager = GetConfigurationManager(); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, XmlSerializer, FileSystemManager); Logger = LoggerFactory.CreateLogger("App"); @@ -532,7 +516,7 @@ namespace Emby.Server.Implementations /// /// Runs the startup tasks. /// - public async Task RunStartupTasks() + public async Task RunStartupTasksAsync() { Logger.LogInformation("Running startup tasks"); @@ -584,7 +568,7 @@ namespace Emby.Server.Implementations } } - public async Task Init(IServiceCollection serviceCollection) + public async Task InitAsync(IServiceCollection serviceCollection) { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -706,8 +690,6 @@ namespace Emby.Server.Implementations serviceCollection.AddLogging(); serviceCollection.AddSingleton(Logger); - serviceCollection.AddSingleton(EnvironmentInfo); - serviceCollection.AddSingleton(FileSystemManager); serviceCollection.AddSingleton(); @@ -786,7 +768,7 @@ namespace Emby.Server.Implementations var musicManager = new MusicManager(LibraryManager); serviceCollection.AddSingleton(new MusicManager(LibraryManager)); - LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager, EnvironmentInfo); + LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager); serviceCollection.AddSingleton(LibraryMonitor); serviceCollection.AddSingleton(new SearchEngine(LoggerFactory, LibraryManager, UserManager)); @@ -907,7 +889,7 @@ namespace Emby.Server.Implementations public virtual string PackageRuntime => "netcore"; - public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo) + public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) { // Distinct these to prevent users from reporting problems that aren't actually problems var commandLineArgs = Environment @@ -915,8 +897,9 @@ namespace Emby.Server.Implementations .Distinct(); logger.LogInformation("Arguments: {Args}", commandLineArgs); - logger.LogInformation("Operating system: {OS} {OSVersion}", environmentInfo.OperatingSystemName, environmentInfo.OperatingSystemVersion); - logger.LogInformation("Architecture: {Architecture}", environmentInfo.SystemArchitecture); + // FIXME: @bond this logs the kernel version, not the OS version + logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version); + logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount); @@ -1400,8 +1383,8 @@ namespace Emby.Server.Implementations HttpServerPortNumber = HttpPort, SupportsHttps = SupportsHttps, HttpsPortNumber = HttpsPort, - OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(), - OperatingSystemDisplayName = EnvironmentInfo.OperatingSystemName, + OperatingSystem = OperatingSystem.Id.ToString(), + OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, WanAddress = wanAddress, @@ -1411,7 +1394,7 @@ namespace Emby.Server.Implementations LocalAddress = localAddress, SupportsLibraryMonitor = true, EncoderLocation = MediaEncoder.EncoderLocation, - SystemArchitecture = EnvironmentInfo.SystemArchitecture, + SystemArchitecture = RuntimeInformation.OSArchitecture, SystemUpdateLevel = SystemUpdateLevel, PackageName = StartupOptions.PackageName }; @@ -1435,7 +1418,7 @@ namespace Emby.Server.Implementations { Version = ApplicationVersion, Id = SystemId, - OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(), + OperatingSystem = OperatingSystem.Id.ToString(), WanAddress = wanAddress, ServerName = FriendlyName, LocalAddress = localAddress diff --git a/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs deleted file mode 100644 index c8104150d6..0000000000 --- a/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using MediaBrowser.Model.System; - -namespace Emby.Server.Implementations.EnvironmentInfo -{ - public class EnvironmentInfo : IEnvironmentInfo - { - public EnvironmentInfo(MediaBrowser.Model.System.OperatingSystem operatingSystem) - { - OperatingSystem = operatingSystem; - } - - public MediaBrowser.Model.System.OperatingSystem OperatingSystem { get; private set; } - - public string OperatingSystemName - { - get - { - switch (OperatingSystem) - { - case MediaBrowser.Model.System.OperatingSystem.Android: return "Android"; - case MediaBrowser.Model.System.OperatingSystem.BSD: return "BSD"; - case MediaBrowser.Model.System.OperatingSystem.Linux: return "Linux"; - case MediaBrowser.Model.System.OperatingSystem.OSX: return "macOS"; - case MediaBrowser.Model.System.OperatingSystem.Windows: return "Windows"; - default: throw new Exception($"Unknown OS {OperatingSystem}"); - } - } - } - - public string OperatingSystemVersion => Environment.OSVersion.Version.ToString() + " " + Environment.OSVersion.ServicePack.ToString(); - - public Architecture SystemArchitecture => RuntimeInformation.OSArchitecture; - } -} diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index d473425115..df4dc41b99 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -10,8 +10,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; -using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.IO { @@ -127,7 +127,6 @@ namespace Emby.Server.Implementations.IO private IServerConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environmentInfo; /// /// Initializes a new instance of the class. @@ -136,14 +135,12 @@ namespace Emby.Server.Implementations.IO ILoggerFactory loggerFactory, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, - IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) + IFileSystem fileSystem) { LibraryManager = libraryManager; Logger = loggerFactory.CreateLogger(GetType().Name); ConfigurationManager = configurationManager; _fileSystem = fileSystem; - _environmentInfo = environmentInfo; } private bool IsLibraryMonitorEnabled(BaseItem item) @@ -267,7 +264,7 @@ namespace Emby.Server.Implementations.IO return; } - if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + if (OperatingSystem.Id != OperatingSystemId.Windows) { if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase)) { @@ -276,12 +273,6 @@ namespace Emby.Server.Implementations.IO } } - if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Android) - { - // causing crashing - return; - } - // Already being watched if (_fileSystemWatchers.ContainsKey(path)) { diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 421592fada..47cea7269d 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.IO { @@ -24,22 +25,19 @@ namespace Emby.Server.Implementations.IO private readonly string _tempPath; - private readonly IEnvironmentInfo _environmentInfo; private readonly bool _isEnvironmentCaseInsensitive; public ManagedFileSystem( ILoggerFactory loggerFactory, - IEnvironmentInfo environmentInfo, IApplicationPaths applicationPaths) { Logger = loggerFactory.CreateLogger("FileSystem"); _supportsAsyncFileStreams = true; _tempPath = applicationPaths.TempDirectory; - _environmentInfo = environmentInfo; - SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows); + SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows); - _isEnvironmentCaseInsensitive = environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; } public virtual void AddShortcutHandler(IShortcutHandler handler) @@ -468,7 +466,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetHidden(string path, bool isHidden) { - if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) { return; } @@ -492,7 +490,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetReadOnly(string path, bool isReadOnly) { - if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) { return; } @@ -516,7 +514,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { - if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + if (OperatingSystem.Id != MediaBrowser.Model.System.OperatingSystemId.Windows) { return; } @@ -801,7 +799,7 @@ namespace Emby.Server.Implementations.IO public virtual void SetExecutable(string path) { - if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) + if (OperatingSystem.Id == MediaBrowser.Model.System.OperatingSystemId.Darwin) { RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); } diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index ace93ebdee..c102f9eb55 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -7,11 +7,11 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.Networking { @@ -22,14 +22,12 @@ namespace Emby.Server.Implementations.Networking public event EventHandler NetworkChanged; public Func LocalSubnetsFn { get; set; } - public NetworkManager( - ILoggerFactory loggerFactory, - IEnvironmentInfo environment) + public NetworkManager(ILoggerFactory loggerFactory) { Logger = loggerFactory.CreateLogger(nameof(NetworkManager)); // In FreeBSD these events cause a crash - if (environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.BSD) + if (OperatingSystem.Id != OperatingSystemId.BSD) { try { diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 17259c7370..8e6ed7a7e5 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -3,7 +3,6 @@ using System.Reflection; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -16,7 +15,6 @@ namespace Jellyfin.Server ILoggerFactory loggerFactory, StartupOptions options, IFileSystem fileSystem, - IEnvironmentInfo environmentInfo, MediaBrowser.Controller.Drawing.IImageEncoder imageEncoder, MediaBrowser.Common.Net.INetworkManager networkManager, IConfiguration configuration) @@ -25,7 +23,6 @@ namespace Jellyfin.Server loggerFactory, options, fileSystem, - environmentInfo, imageEncoder, networkManager, configuration) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0ef1711d4c..a60f2a4f61 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using CommandLine; using Emby.Drawing; using Emby.Server.Implementations; -using Emby.Server.Implementations.EnvironmentInfo; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; @@ -46,7 +45,6 @@ namespace Jellyfin.Server const string pattern = @"^(-[^-\s]{2})"; // Match -xx, not -x, not --xx, not xx const string substitution = @"-$1"; // Prepend with additional single-hyphen var regex = new Regex(pattern); - for (var i = 0; i < args.Length; i++) { args[i] = regex.Replace(args[i], substitution); @@ -54,9 +52,7 @@ namespace Jellyfin.Server // Parse the command line arguments and either start the app or exit indicating error await Parser.Default.ParseArguments(args) - .MapResult( - options => StartApp(options), - errs => Task.FromResult(0)).ConfigureAwait(false); + .MapResult(StartApp, _ => Task.CompletedTask).ConfigureAwait(false); } public static void Shutdown() @@ -119,31 +115,29 @@ namespace Jellyfin.Server _logger.LogInformation("Jellyfin version: {Version}", Assembly.GetEntryAssembly().GetName().Version); - EnvironmentInfo environmentInfo = new EnvironmentInfo(GetOperatingSystem()); - ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo); + ApplicationHost.LogEnvironmentInfo(_logger, appPaths); SQLitePCL.Batteries_V2.Init(); // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); - var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths); + var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths); using (var appHost = new CoreAppHost( appPaths, _loggerFactory, options, fileSystem, - environmentInfo, new NullImageEncoder(), - new NetworkManager(_loggerFactory, environmentInfo), + new NetworkManager(_loggerFactory), appConfig)) { - await appHost.Init(new ServiceCollection()).ConfigureAwait(false); + await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false); appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); - await appHost.RunStartupTasks().ConfigureAwait(false); + await appHost.RunStartupTasksAsync().ConfigureAwait(false); try { @@ -179,7 +173,6 @@ namespace Jellyfin.Server // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin // ELSE use $HOME/.local/share/jellyfin var dataDir = options.DataDir; - if (string.IsNullOrEmpty(dataDir)) { dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH"); @@ -236,7 +229,6 @@ namespace Jellyfin.Server // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin // ELSE HOME/.cache/jellyfin var cacheDir = options.CacheDir; - if (string.IsNullOrEmpty(cacheDir)) { cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); @@ -270,7 +262,6 @@ namespace Jellyfin.Server // ELSE IF --datadir, use /log (assume portable run) // ELSE /log var logDir = options.LogDir; - if (string.IsNullOrEmpty(logDir)) { logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); @@ -364,36 +355,6 @@ namespace Jellyfin.Server return new NullImageEncoder(); } - private static MediaBrowser.Model.System.OperatingSystem GetOperatingSystem() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return MediaBrowser.Model.System.OperatingSystem.OSX; - case PlatformID.Win32NT: - return MediaBrowser.Model.System.OperatingSystem.Windows; - case PlatformID.Unix: - default: - { - string osDescription = RuntimeInformation.OSDescription; - if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.Linux; - } - else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.OSX; - } - else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.BSD; - } - - throw new Exception($"Can't resolve OS with description: '{osDescription}'"); - } - } - } - private static void StartNewInstance(StartupOptions options) { _logger.LogInformation("Starting new instance"); diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 88ed734569..486d5e8a71 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -698,12 +698,11 @@ namespace MediaBrowser.Api.LiveTv private readonly IFileSystem _fileSystem; private readonly IAuthorizationContext _authContext; private readonly ISessionContext _sessionContext; - private readonly IEnvironmentInfo _environment; private ICryptoProvider _cryptographyProvider; private IStreamHelper _streamHelper; private IMediaSourceManager _mediaSourceManager; - public LiveTvService(ICryptoProvider crypto, IMediaSourceManager mediaSourceManager, IStreamHelper streamHelper, ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem, IAuthorizationContext authContext, ISessionContext sessionContext, IEnvironmentInfo environment) + public LiveTvService(ICryptoProvider crypto, IMediaSourceManager mediaSourceManager, IStreamHelper streamHelper, ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem, IAuthorizationContext authContext, ISessionContext sessionContext) { _liveTvManager = liveTvManager; _userManager = userManager; @@ -714,7 +713,6 @@ namespace MediaBrowser.Api.LiveTv _fileSystem = fileSystem; _authContext = authContext; _sessionContext = sessionContext; - _environment = environment; _cryptographyProvider = crypto; _streamHelper = streamHelper; _mediaSourceManager = mediaSourceManager; @@ -756,7 +754,7 @@ namespace MediaBrowser.Api.LiveTv [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path) }; - return new ProgressiveFileCopier(_fileSystem, _streamHelper, path, outputHeaders, Logger, _environment) + return new ProgressiveFileCopier(_fileSystem, _streamHelper, path, outputHeaders, Logger) { AllowEndOfFile = false }; @@ -779,7 +777,7 @@ namespace MediaBrowser.Api.LiveTv [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container) }; - return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger, _environment) + return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger) { AllowEndOfFile = false }; diff --git a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs index 8412bf66b9..51552d928b 100644 --- a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs +++ b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.LiveTv @@ -23,25 +22,22 @@ namespace MediaBrowser.Api.LiveTv public bool AllowEndOfFile = true; private readonly IDirectStreamProvider _directStreamProvider; - private readonly IEnvironmentInfo _environment; private IStreamHelper _streamHelper; - public ProgressiveFileCopier(IFileSystem fileSystem, IStreamHelper streamHelper, string path, Dictionary outputHeaders, ILogger logger, IEnvironmentInfo environment) + public ProgressiveFileCopier(IFileSystem fileSystem, IStreamHelper streamHelper, string path, Dictionary outputHeaders, ILogger logger) { _fileSystem = fileSystem; _path = path; _outputHeaders = outputHeaders; _logger = logger; - _environment = environment; _streamHelper = streamHelper; } - public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, IStreamHelper streamHelper, Dictionary outputHeaders, ILogger logger, IEnvironmentInfo environment) + public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, IStreamHelper streamHelper, Dictionary outputHeaders, ILogger logger) { _directStreamProvider = directStreamProvider; _outputHeaders = outputHeaders; _logger = logger; - _environment = environment; _streamHelper = streamHelper; } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 48b4e2f24e..dfe4b2b8e9 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; @@ -11,7 +10,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; namespace MediaBrowser.Api.Playback.Progressive { @@ -46,8 +44,7 @@ namespace MediaBrowser.Api.Playback.Progressive IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext, - IEnvironmentInfo environmentInfo) + IAuthorizationContext authorizationContext) : base(httpClient, serverConfig, userManager, @@ -60,8 +57,7 @@ namespace MediaBrowser.Api.Playback.Progressive deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext, - environmentInfo) + authorizationContext) { } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index f43f26b2f3..a2c20e38fd 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -8,15 +7,12 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Playback.Progressive @@ -26,7 +22,6 @@ namespace MediaBrowser.Api.Playback.Progressive /// public abstract class BaseProgressiveStreamingService : BaseStreamingService { - protected readonly IEnvironmentInfo EnvironmentInfo; protected IHttpClient HttpClient { get; private set; } public BaseProgressiveStreamingService( @@ -42,8 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext, - IEnvironmentInfo environmentInfo) + IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, @@ -57,7 +51,6 @@ namespace MediaBrowser.Api.Playback.Progressive jsonSerializer, authorizationContext) { - EnvironmentInfo = environmentInfo; HttpClient = httpClient; } @@ -157,7 +150,7 @@ namespace MediaBrowser.Api.Playback.Progressive // TODO: Don't hardcode this outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts"); - return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) + return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None) { AllowEndOfFile = false }; @@ -203,7 +196,7 @@ namespace MediaBrowser.Api.Playback.Progressive }; - return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) + return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, CancellationToken.None) { AllowEndOfFile = false }; @@ -397,7 +390,7 @@ namespace MediaBrowser.Api.Playback.Progressive outputHeaders[item.Key] = item.Value; } - return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, EnvironmentInfo, CancellationToken.None); + return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None); } finally { diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 3dd9de2a1b..6609120655 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -8,6 +8,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace MediaBrowser.Api.Playback.Progressive { @@ -27,9 +28,8 @@ namespace MediaBrowser.Api.Playback.Progressive public bool AllowEndOfFile = true; private readonly IDirectStreamProvider _directStreamProvider; - private readonly IEnvironmentInfo _environment; - public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken) + public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) { _fileSystem = fileSystem; _path = path; @@ -37,17 +37,15 @@ namespace MediaBrowser.Api.Playback.Progressive _job = job; _logger = logger; _cancellationToken = cancellationToken; - _environment = environment; } - public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken) + public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) { _directStreamProvider = directStreamProvider; _outputHeaders = outputHeaders; _job = job; _logger = logger; _cancellationToken = cancellationToken; - _environment = environment; } public IDictionary Headers => _outputHeaders; @@ -79,7 +77,7 @@ namespace MediaBrowser.Api.Playback.Progressive var eofCount = 0; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - var allowAsyncFileRead = _environment.OperatingSystem != Model.System.OperatingSystem.Windows; + var allowAsyncFileRead = OperatingSystem.Id != OperatingSystemId.Windows; using (var inputStream = GetInputStream(allowAsyncFileRead)) { diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index bf15cc756c..ab19fdc261 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; @@ -11,7 +10,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; namespace MediaBrowser.Api.Playback.Progressive { @@ -83,8 +81,7 @@ namespace MediaBrowser.Api.Playback.Progressive IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext, - IEnvironmentInfo environmentInfo) + IAuthorizationContext authorizationContext) : base(httpClient, serverConfig, userManager, @@ -97,8 +94,7 @@ namespace MediaBrowser.Api.Playback.Progressive deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext, - environmentInfo) + authorizationContext) { } diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index f97e88e986..b3d8bfe59f 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -18,7 +18,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback @@ -93,7 +92,6 @@ namespace MediaBrowser.Api.Playback IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, INetworkManager networkManager, - IEnvironmentInfo environmentInfo, ILoggerFactory loggerFactory) { HttpClient = httpClient; @@ -112,7 +110,6 @@ namespace MediaBrowser.Api.Playback AuthorizationContext = authorizationContext; ImageProcessor = imageProcessor; NetworkManager = networkManager; - EnvironmentInfo = environmentInfo; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(nameof(UniversalAudioService)); } @@ -133,7 +130,6 @@ namespace MediaBrowser.Api.Playback protected IAuthorizationContext AuthorizationContext { get; private set; } protected IImageProcessor ImageProcessor { get; private set; } protected INetworkManager NetworkManager { get; private set; } - protected IEnvironmentInfo EnvironmentInfo { get; private set; } private ILoggerFactory _loggerFactory; private ILogger _logger; @@ -338,8 +334,7 @@ namespace MediaBrowser.Api.Playback DeviceManager, MediaSourceManager, JsonSerializer, - AuthorizationContext, - EnvironmentInfo) + AuthorizationContext) { Request = Request }; diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 3a4098612d..966448d630 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Events; @@ -107,7 +106,7 @@ namespace MediaBrowser.Common /// /// Inits this instance. /// - Task Init(IServiceCollection serviceCollection); + Task InitAsync(IServiceCollection serviceCollection); /// /// Creates the instance. diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs new file mode 100644 index 0000000000..640821d4d7 --- /dev/null +++ b/MediaBrowser.Common/System/OperatingSystem.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using MediaBrowser.Model.System; + +namespace MediaBrowser.Common.System +{ + public static class OperatingSystem + { + // We can't use Interlocked.CompareExchange for enums + private static int _id = int.MaxValue; + + public static OperatingSystemId Id + { + get + { + if (_id == int.MaxValue) + { + Interlocked.CompareExchange(ref _id, (int)GetId(), int.MaxValue); + } + + return (OperatingSystemId)_id; + } + } + + public static string Name + { + get + { + switch (Id) + { + case OperatingSystemId.BSD: return "BSD"; + case OperatingSystemId.Linux: return "Linux"; + case OperatingSystemId.Darwin: return "macOS"; + case OperatingSystemId.Windows: return "Windows"; + default: throw new Exception($"Unknown OS {Id}"); + } + } + } + + private static OperatingSystemId GetId() + { + switch (Environment.OSVersion.Platform) + { + // On .NET Core `MacOSX` got replaced by `Unix`, this case should never be hit. + case PlatformID.MacOSX: + return OperatingSystemId.Darwin; + case PlatformID.Win32NT: + return OperatingSystemId.Windows; + case PlatformID.Unix: + default: + { + string osDescription = RuntimeInformation.OSDescription; + if (osDescription.IndexOf("linux", StringComparison.OrdinalIgnoreCase) != -1) + { + return OperatingSystemId.Linux; + } + else if (osDescription.IndexOf("darwin", StringComparison.OrdinalIgnoreCase) != -1) + { + return OperatingSystemId.Darwin; + } + else if (osDescription.IndexOf("bsd", StringComparison.OrdinalIgnoreCase) != -1) + { + return OperatingSystemId.BSD; + } + + throw new Exception($"Can't resolve OS with description: '{osDescription}'"); + } + } + } + } +} diff --git a/MediaBrowser.Model/System/IEnvironmentInfo.cs b/MediaBrowser.Model/System/IEnvironmentInfo.cs deleted file mode 100644 index 3ffcc7de14..0000000000 --- a/MediaBrowser.Model/System/IEnvironmentInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace MediaBrowser.Model.System -{ - public interface IEnvironmentInfo - { - OperatingSystem OperatingSystem { get; } - string OperatingSystemName { get; } - string OperatingSystemVersion { get; } - Architecture SystemArchitecture { get; } - } - - public enum OperatingSystem - { - Windows, - Linux, - OSX, - BSD, - Android - } -} diff --git a/MediaBrowser.Model/System/OperatingSystemId.cs b/MediaBrowser.Model/System/OperatingSystemId.cs new file mode 100644 index 0000000000..e81dd4213f --- /dev/null +++ b/MediaBrowser.Model/System/OperatingSystemId.cs @@ -0,0 +1,10 @@ +namespace MediaBrowser.Model.System +{ + public enum OperatingSystemId + { + Windows, + Linux, + Darwin, + BSD + } +} From e3b844b5aa34ef6a8f4a23c053115f4eabadcbf7 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 7 Mar 2019 22:49:41 +0100 Subject: [PATCH 0186/6208] Add urlprefixes during init --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 6 ++++-- MediaBrowser.Controller/Net/IHttpServer.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a581214c71..f7d9bad1b0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1056,7 +1056,7 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - HttpServer.Init(GetExports(false), GetExports()); + HttpServer.Init(GetExports(false), GetExports(), GetUrlPrefixes()); LibraryManager.AddParts(GetExports(), GetExports(), diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index eab755cefb..e8d47cad52 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -676,10 +676,12 @@ namespace Emby.Server.Implementations.HttpServer /// Adds the rest handlers. /// /// The services. - public void Init(IEnumerable services, IEnumerable listeners) + /// + /// + public void Init(IEnumerable services, IEnumerable listeners, IEnumerable urlPrefixes) { _webSocketListeners = listeners.ToArray(); - + UrlPrefixes = urlPrefixes.ToArray(); ServiceController = new ServiceController(); Logger.LogInformation("Calling ServiceStack AppHost.Init"); diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index cede9a98fb..46933c0465 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Net /// /// Inits this instance. /// - void Init(IEnumerable services, IEnumerable listener); + void Init(IEnumerable services, IEnumerable listener, IEnumerable urlPrefixes); /// /// If set, all requests will respond with this message From 4625592a8377b1eb04974f39bc220a362e960ca4 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 7 Mar 2019 22:52:41 +0100 Subject: [PATCH 0187/6208] Quick nullref fix --- MediaBrowser.Controller/Entities/BaseItem.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 39b6eedfeb..154b9470ef 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -36,6 +36,12 @@ namespace MediaBrowser.Controller.Entities /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo { + /// + /// The supported image extensions + /// + public static readonly IReadOnlyList SupportedImageExtensions + = new [] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + private static readonly List _supportedExtensions = new List(SupportedImageExtensions) { ".nfo", @@ -69,11 +75,6 @@ namespace MediaBrowser.Controller.Entities public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; public static char SlugChar = '-'; - /// - /// The supported image extensions - /// - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; - /// /// The trailer folder name /// From 80fb3dc2cb720269050a7a2d952bbb269c9e88c8 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 06:36:00 +0100 Subject: [PATCH 0188/6208] Fix error --- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 154b9470ef..e20641c99a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Entities /// /// The supported image extensions /// - public static readonly IReadOnlyList SupportedImageExtensions + public static readonly string[] SupportedImageExtensions = new [] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; private static readonly List _supportedExtensions = new List(SupportedImageExtensions) From 045a0419f6983acfffe53591d24386c6c59d3901 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Fri, 8 Mar 2019 02:33:08 -0500 Subject: [PATCH 0189/6208] remove new file header text, and remove it from solution This removes the new file header text file from the repository, and also removes the include that was in the project solution. --- MediaBrowser.sln | 2 +- new-file-header.txt | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 new-file-header.txt diff --git a/MediaBrowser.sln b/MediaBrowser.sln index e43a582242..a3c0569bad 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -180,7 +180,7 @@ Global GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.StandardHeader = $1 - $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n + $1.IncludeInNewFiles = False $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection diff --git a/new-file-header.txt b/new-file-header.txt deleted file mode 100644 index 4247a8e246..0000000000 --- a/new-file-header.txt +++ /dev/null @@ -1,22 +0,0 @@ -### This header should be used to start new files. -### It provides an explicit per-file license reference that should be present on all new files. -### To use this header, delete these lines and the following empty line, modify to -### the proper full path, and then add new code following the header and a single empty line. - -// -// Part of the Jellyfin project (https://jellyfin.media) -// -// All copyright belongs to the Jellyfin contributors; a full list can -// be found in the file CONTRIBUTORS.md -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 2. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . From e498e471091c8dd6f03b71c9f269dd546bc8dde3 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 8 Mar 2019 18:39:41 +0900 Subject: [PATCH 0190/6208] remove mirror images from library thumbnail --- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 52 ++------------------ 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index dfdf398710..f2162432a9 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -78,20 +78,15 @@ namespace Jellyfin.Drawing.Skia canvas.Clear(SKColors.Black); // determine sizes for each image that will composited into the final image - var iSlice = Convert.ToInt32(width * 0.23475); - int iTrans = Convert.ToInt32(height * .25); - int iHeight = Convert.ToInt32(height * .70); - var horizontalImagePadding = Convert.ToInt32(width * 0.0125); - var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + var iSlice = Convert.ToInt32(width * 0.33); + int iTrans = Convert.ToInt32(height * 0.25); + int iHeight = Convert.ToInt32(height * 1.00); int imageIndex = 0; - - for (int i = 0; i < 4; i++) + for (int i = 0; i < 3; i++) { - using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) { imageIndex = newIndex; - if (currentBitmap == null) { continue; @@ -108,44 +103,7 @@ namespace Jellyfin.Drawing.Skia using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) { // draw image onto canvas - canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing); - - if (subset == null) - { - continue; - } - // create reflection of image below the drawn image - using (var croppedBitmap = SKBitmap.FromImage(subset)) - using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType)) - { - // resize to half height - currentBitmap.ScalePixels(reflectionBitmap, SKFilterQuality.High); - - using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType)) - using (var flippedCanvas = new SKCanvas(flippedBitmap)) - { - // flip image vertically - var matrix = SKMatrix.MakeScale(1, -1); - matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height); - flippedCanvas.SetMatrix(matrix); - flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0); - flippedCanvas.ResetMatrix(); - - // create gradient to make image appear as a reflection - var remainingHeight = height - (iHeight + (2 * verticalSpacing)); - flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight)); - using (var gradient = new SKPaint()) - { - gradient.IsAntialias = true; - gradient.BlendMode = SKBlendMode.SrcOver; - gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp); - flippedCanvas.DrawPaint(gradient); - } - - // finally draw reflection onto canvas - canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing)); - } - } + canvas.DrawImage(subset ?? image, iSlice * i, 0); } } } From 58061a729528e3969b07b35fdaac534f5948eb89 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 17:15:52 +0100 Subject: [PATCH 0191/6208] Make MusicBrainz base url configurable --- Emby.Server.Implementations/ConfigurationOptions.cs | 3 ++- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 1 + .../Music/MusicBrainzAlbumProvider.cs | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 30bfd87498..9bc60972a1 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -6,7 +6,8 @@ namespace Emby.Server.Implementations { public static readonly Dictionary Configuration = new Dictionary { - {"HttpListenerHost:DefaultRedirectPath", "web/index.html"} + {"HttpListenerHost:DefaultRedirectPath", "web/index.html"}, + {"MusicBrainz:BaseUrl", "https://www.musicbrainz.org"} }; } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 52a52efdc0..cfbb85ea6b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,6 +11,7 @@ + diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index e4bb52217c..7e47c0b2a5 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -16,6 +16,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Xml; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Music @@ -30,15 +31,22 @@ namespace MediaBrowser.Providers.Music private readonly IJsonSerializer _json; private readonly IXmlReaderSettingsFactory _xmlSettings; - public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org"; + public readonly string MusicBrainzBaseUrl; - public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json, IXmlReaderSettingsFactory xmlSettings) + public MusicBrainzAlbumProvider( + IHttpClient httpClient, + IApplicationHost appHost, + ILogger logger, + IJsonSerializer json, + IXmlReaderSettingsFactory xmlSettings, + IConfiguration configuration) { _httpClient = httpClient; _appHost = appHost; _logger = logger; _json = json; _xmlSettings = xmlSettings; + MusicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; Current = this; } From 369785c184a8e19250c6e3b16b3609c222399552 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 20:17:17 +0100 Subject: [PATCH 0192/6208] Remove usage of depricated 'WebRequest' Ref: https://docs.microsoft.com/en-us/dotnet/api/system.net.webrequest?view=netframework-4.7.2 --- .../HttpClientManager/HttpClientInfo.cs | 18 - .../HttpClientManager/HttpClientManager.cs | 438 ++++++------------ MediaBrowser.Common/Net/HttpRequestOptions.cs | 1 - MediaBrowser.Common/Net/IHttpClient.cs | 11 + 4 files changed, 164 insertions(+), 304 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs deleted file mode 100644 index f747b01b93..0000000000 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Net.Http; - -namespace Emby.Server.Implementations.HttpClientManager -{ - /// - /// Class HttpClientInfo - /// - public class HttpClientInfo - { - /// - /// Gets or sets the last timeout. - /// - /// The last timeout. - public DateTime LastTimeout { get; set; } - public HttpClient HttpClient { get; set; } - } -} diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 1bebdd1637..581d6bafd1 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Cache; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -55,12 +54,13 @@ namespace Emby.Server.Implementations.HttpClientManager { throw new ArgumentNullException(nameof(appPaths)); } + if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } - _logger = loggerFactory.CreateLogger("HttpClient"); + _logger = loggerFactory.CreateLogger(nameof(HttpClientManager)); _fileSystem = fileSystem; _appPaths = appPaths; _defaultUserAgentFn = defaultUserAgentFn; @@ -74,27 +74,26 @@ namespace Emby.Server.Implementations.HttpClientManager /// DON'T dispose it after use. /// /// The HTTP clients. - private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); /// /// Gets /// - /// The host. + /// The host. /// if set to true [enable HTTP compression]. /// HttpClient. /// host - private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression) + private HttpClient GetHttpClient(string url, bool enableHttpCompression) { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - var key = host + enableHttpCompression; + var key = GetHostFromUrl(url) + enableHttpCompression; if (!_httpClients.TryGetValue(key, out var client)) { - client = new HttpClientInfo(); + + client = new HttpClient() + { + BaseAddress = new Uri(url) + }; _httpClients.TryAdd(key, client); } @@ -102,110 +101,87 @@ namespace Emby.Server.Implementations.HttpClientManager return client; } - private WebRequest GetRequest(HttpRequestOptions options, string method) + private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method) { string url = options.Url; - var uriAddress = new Uri(url); string userInfo = uriAddress.UserInfo; if (!string.IsNullOrWhiteSpace(userInfo)) { - _logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url); + _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); url = url.Replace(userInfo + "@", string.Empty); } - var request = WebRequest.Create(url); + var request = new HttpRequestMessage(method, url); - if (request is HttpWebRequest httpWebRequest) + AddRequestHeaders(request, options); + + if (options.EnableHttpCompression) { - AddRequestHeaders(httpWebRequest, options); - - if (options.EnableHttpCompression) + if (options.DecompressionMethod.HasValue + && options.DecompressionMethod.Value == CompressionMethod.Gzip) { - httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate; - if (options.DecompressionMethod.HasValue - && options.DecompressionMethod.Value == CompressionMethod.Gzip) - { - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip; - } + request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); } else { - httpWebRequest.AutomaticDecompression = DecompressionMethods.None; - } - - httpWebRequest.KeepAlive = options.EnableKeepAlive; - - if (!string.IsNullOrEmpty(options.Host)) - { - httpWebRequest.Host = options.Host; - } - - if (!string.IsNullOrEmpty(options.Referer)) - { - httpWebRequest.Referer = options.Referer; + request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); } } - request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); + if (options.EnableKeepAlive) + { + request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); + } - request.Method = method; - request.Timeout = options.TimeoutMs; + if (!string.IsNullOrEmpty(options.Host)) + { + request.Headers.Add(HeaderNames.Host, options.Host); + } + if (!string.IsNullOrEmpty(options.Referer)) + { + request.Headers.Add(HeaderNames.Referer, options.Referer); + } + + //request.Headers.Add(HeaderNames.CacheControl, "no-cache"); + + //request.Headers.Add(HeaderNames., options.TimeoutMs; + + /* if (!string.IsNullOrWhiteSpace(userInfo)) { var parts = userInfo.Split(':'); if (parts.Length == 2) { - request.Credentials = GetCredential(url, parts[0], parts[1]); - // TODO: .net core ?? - request.PreAuthenticate = true; + request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]); } } + */ return request; } - private static CredentialCache GetCredential(string url, string username, string password) - { - //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3; - var credentialCache = new CredentialCache(); - credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password)); - return credentialCache; - } - - private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options) + private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options) { var hasUserAgent = false; foreach (var header in options.RequestHeaders) { - if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) { - request.Accept = header.Value; - } - else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) - { - SetUserAgent(request, header.Value); hasUserAgent = true; } - else - { - request.Headers.Set(header.Key, header.Value); - } + + request.Headers.Add(header.Key, header.Value); } if (!hasUserAgent && options.EnableDefaultUserAgent) { - SetUserAgent(request, _defaultUserAgentFn()); + request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn()); } } - private static void SetUserAgent(HttpWebRequest request, string userAgent) - { - request.UserAgent = userAgent; - } - /// /// Gets the response internal. /// @@ -213,7 +189,7 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{HttpResponseInfo}. public Task GetResponse(HttpRequestOptions options) { - return SendAsync(options, "GET"); + return SendAsync(options, HttpMethod.Get); } /// @@ -235,7 +211,21 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{HttpResponseInfo}. /// /// - public async Task SendAsync(HttpRequestOptions options, string httpMethod) + public Task SendAsync(HttpRequestOptions options, string httpMethod) + { + var httpMethod2 = GetHttpMethod(httpMethod); + return SendAsync(options, httpMethod2); + } + + /// + /// send as an asynchronous operation. + /// + /// The options. + /// The HTTP method. + /// Task{HttpResponseInfo}. + /// + /// + public async Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod) { if (options.CacheMode == CacheMode.None) { @@ -263,6 +253,40 @@ namespace Emby.Server.Implementations.HttpClientManager return response; } + private HttpMethod GetHttpMethod(string httpMethod) + { + if (httpMethod.Equals("DELETE", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Delete; + } + else if (httpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Get; + } + else if (httpMethod.Equals("HEAD", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Head; + } + else if (httpMethod.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Options; + } + else if (httpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Post; + } + else if (httpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Put; + } + else if (httpMethod.Equals("TRACE", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Trace; + } + + throw new ArgumentException("Invalid HTTP method", nameof(httpMethod)); + } + private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) { if (File.Exists(responseCachePath) @@ -294,31 +318,23 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private async Task SendAsyncInternal(HttpRequestOptions options, string httpMethod) + private async Task SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod) { ValidateParams(options); options.CancellationToken.ThrowIfCancellationRequested(); - var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); + var client = GetHttpClient(options.Url, options.EnableHttpCompression); - if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds) - { - throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) - { - IsTimedOut = true - }; - } - - var httpWebRequest = GetRequest(options, httpMethod); + var httpWebRequest = GetRequestMessage(options, httpMethod); if (options.RequestContentBytes != null || !string.IsNullOrEmpty(options.RequestContent) || - string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase)) + httpMethod == HttpMethod.Post) { try { - var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty); + httpWebRequest.Content = new StringContent(Encoding.UTF8.GetString(options.RequestContentBytes) ?? options.RequestContent ?? string.Empty); var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; @@ -327,8 +343,8 @@ namespace Emby.Server.Implementations.HttpClientManager contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\""; } - httpWebRequest.ContentType = contentType; - (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); + httpWebRequest.Headers.Add(HeaderNames.ContentType, contentType); + await client.SendAsync(httpWebRequest).ConfigureAwait(false); } catch (Exception ex) { @@ -341,68 +357,45 @@ namespace Emby.Server.Implementations.HttpClientManager await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); } - if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds) - { - options.ResourcePool?.Release(); - - throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true }; - } - if (options.LogRequest) { - if (options.LogRequestAsDebug) - { - _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url); - } - else - { - _logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url); - } + _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); } try { options.CancellationToken.ThrowIfCancellationRequested(); - if (!options.BufferContent) + /*if (!options.BufferContent) { - var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false); + var response = await client.HttpClient.SendAsync(httpWebRequest).ConfigureAwait(false); - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(client, response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse); - } + return GetResponseInfo(response, await response.Content.ReadAsStreamAsync().ConfigureAwait(false), response.Content.Headers.ContentLength, response); + }*/ - using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false)) + using (var response = await client.SendAsync(httpWebRequest).ConfigureAwait(false)) { - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - using (var stream = httpResponse.GetResponseStream()) + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; - return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null); + return GetResponseInfo(response, memoryStream, memoryStream.Length, null); } } } catch (OperationCanceledException ex) { - throw GetCancellationException(options, client, options.CancellationToken, ex); - } - catch (Exception ex) - { - throw GetException(ex, options, client); + throw GetCancellationException(options, options.CancellationToken, ex); } finally { @@ -410,69 +403,54 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable) + private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, Stream content, long? contentLength, IDisposable disposable) { var responseInfo = new HttpResponseInfo(disposable) { Content = content, StatusCode = httpResponse.StatusCode, - ContentType = httpResponse.ContentType, + ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ContentLength = contentLength, - ResponseUrl = httpResponse.ResponseUri.ToString() + ResponseUrl = httpResponse.Content.Headers.ContentLocation?.ToString() }; if (httpResponse.Headers != null) { - SetHeaders(httpResponse.Headers, responseInfo); + SetHeaders(httpResponse.Content.Headers, responseInfo); } return responseInfo; } - private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength) + private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, string tempFile, long? contentLength) { var responseInfo = new HttpResponseInfo { TempFilePath = tempFile, StatusCode = httpResponse.StatusCode, - ContentType = httpResponse.ContentType, + ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ContentLength = contentLength }; if (httpResponse.Headers != null) { - SetHeaders(httpResponse.Headers, responseInfo); + SetHeaders(httpResponse.Content.Headers, responseInfo); } return responseInfo; } - private static void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo) + private static void SetHeaders(HttpContentHeaders headers, HttpResponseInfo responseInfo) { - foreach (var key in headers.AllKeys) + foreach (var key in headers) { - responseInfo.Headers[key] = headers[key]; + responseInfo.Headers[key.Key] = string.Join(", ", key.Value); } } public Task Post(HttpRequestOptions options) { - return SendAsync(options, "POST"); - } - - /// - /// Performs a POST request - /// - /// The options. - /// Params to add to the POST data. - /// stream on success, null on failure - public async Task Post(HttpRequestOptions options, Dictionary postData) - { - options.SetPostData(postData); - - var response = await Post(options).ConfigureAwait(false); - - return response.Content; + return SendAsync(options, HttpMethod.Post); } /// @@ -482,9 +460,10 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{System.String}. public async Task GetTempFile(HttpRequestOptions options) { - var response = await GetTempFileResponse(options).ConfigureAwait(false); - - return response.TempFilePath; + using (var response = await GetTempFileResponse(options).ConfigureAwait(false)) + { + return response.TempFilePath; + } } public async Task GetTempFileResponse(HttpRequestOptions options) @@ -502,7 +481,7 @@ namespace Emby.Server.Implementations.HttpClientManager options.CancellationToken.ThrowIfCancellationRequested(); - var httpWebRequest = GetRequest(options, "GET"); + var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); if (options.ResourcePool != null) { @@ -513,33 +492,22 @@ namespace Emby.Server.Implementations.HttpClientManager if (options.LogRequest) { - if (options.LogRequestAsDebug) - { - _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } - else - { - _logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } + _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); } - var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); + var client = GetHttpClient(options.Url, options.EnableHttpCompression); try { options.CancellationToken.ThrowIfCancellationRequested(); - using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false)) + using (var response = (await client.SendAsync(httpWebRequest).ConfigureAwait(false))) { - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - var contentLength = GetContentLength(httpResponse); - - using (var stream = httpResponse.GetResponseStream()) + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); @@ -547,13 +515,18 @@ namespace Emby.Server.Implementations.HttpClientManager options.Progress.Report(100); - return GetResponseInfo(httpResponse, tempFile, contentLength); + var contentLength = response.Content.Headers.ContentLength; + return GetResponseInfo(response, tempFile, contentLength); } } catch (Exception ex) { - DeleteTempFile(tempFile); - throw GetException(ex, options, client); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + throw GetException(ex, options); } finally { @@ -561,21 +534,7 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private static long? GetContentLength(HttpWebResponse response) - { - var length = response.ContentLength; - - if (length == 0) - { - return null; - } - - return length; - } - - protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client) + private Exception GetException(Exception ex, HttpRequestOptions options) { if (ex is HttpException) { @@ -599,11 +558,6 @@ namespace Emby.Server.Implementations.HttpClientManager if (response != null) { exception.StatusCode = response.StatusCode; - - if ((int)response.StatusCode == 429) - { - client.LastTimeout = DateTime.UtcNow; - } } } @@ -624,7 +578,7 @@ namespace Emby.Server.Implementations.HttpClientManager if (operationCanceledException != null) { - return GetCancellationException(options, client, options.CancellationToken, operationCanceledException); + return GetCancellationException(options, options.CancellationToken, operationCanceledException); } if (options.LogErrors) @@ -635,18 +589,6 @@ namespace Emby.Server.Implementations.HttpClientManager return ex; } - private void DeleteTempFile(string file) - { - try - { - _fileSystem.DeleteFile(file); - } - catch (IOException) - { - // Might not have been created at all. No need to worry. - } - } - private void ValidateParams(HttpRequestOptions options) { if (string.IsNullOrEmpty(options.Url)) @@ -682,11 +624,10 @@ namespace Emby.Server.Implementations.HttpClientManager /// Throws the cancellation exception. /// /// The options. - /// The client. /// The cancellation token. /// The exception. /// Exception. - private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception) + private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception) { // If the HttpClient's timeout is reached, it will cancel the Task internally if (!cancellationToken.IsCancellationRequested) @@ -698,8 +639,6 @@ namespace Emby.Server.Implementations.HttpClientManager _logger.LogError(msg); } - client.LastTimeout = DateTime.UtcNow; - // Throw an HttpException so that the caller doesn't think it was cancelled by user code return new HttpException(msg, exception) { @@ -710,91 +649,20 @@ namespace Emby.Server.Implementations.HttpClientManager return exception; } - private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options) + private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) { - var statusCode = response.StatusCode; - - var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299; - - if (isSuccessful) + if (response.IsSuccessStatusCode) { return; } - if (options.LogErrorResponseBody) - { - try - { - using (var stream = response.GetResponseStream()) - { - if (stream != null) - { - using (var reader = new StreamReader(stream)) - { - var msg = reader.ReadToEnd(); + var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogError(msg); - _logger.LogError(msg); - } - } - } - } - catch - { - - } - } - - throw new HttpException(response.StatusDescription) + throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode }; } - - private static Task GetResponseAsync(WebRequest request, TimeSpan timeout) - { - var taskCompletion = new TaskCompletionSource(); - - var asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null); - - ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true); - var callback = new TaskCallback { taskCompletion = taskCompletion }; - asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted); - - // Handle errors - asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted); - - return taskCompletion.Task; - } - - private static void TimeoutCallback(object state, bool timedOut) - { - if (timedOut && state != null) - { - var request = (WebRequest)state; - request.Abort(); - } - } - - private class TaskCallback - { - public TaskCompletionSource taskCompletion; - - public void OnSuccess(Task task) - { - taskCompletion.TrySetResult(task.Result); - } - - public void OnError(Task task) - { - if (task.Exception == null) - { - taskCompletion.TrySetException(Enumerable.Empty()); - } - else - { - taskCompletion.TrySetException(task.Exception); - } - } - } } } diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index bea178517b..874383fedf 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -99,7 +99,6 @@ namespace MediaBrowser.Common.Net public bool EnableDefaultUserAgent { get; set; } public bool AppendCharsetToMimeType { get; set; } - public string DownloadFilePath { get; set; } private string GetHeaderValue(string name) { diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index 5aaf7e0be6..d332ab2000 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading.Tasks; +using System.Net.Http; namespace MediaBrowser.Common.Net { @@ -23,6 +24,8 @@ namespace MediaBrowser.Common.Net Task Get(HttpRequestOptions options); /// + /// Warning: Depricated function, + /// use 'Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead /// Sends the asynchronous. /// /// The options. @@ -30,6 +33,14 @@ namespace MediaBrowser.Common.Net /// Task{HttpResponseInfo}. Task SendAsync(HttpRequestOptions options, string httpMethod); + /// + /// Sends the asynchronous. + /// + /// The options. + /// The HTTP method. + /// Task{HttpResponseInfo}. + Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod); + /// /// Posts the specified options. /// From 7f42dcc60fd3aaf30f2408f6bddeb2b8bf921cdf Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 20:32:14 +0100 Subject: [PATCH 0193/6208] Remove more unused stuff --- .../ApplicationHost.cs | 11 +++++----- .../HttpClientManager/HttpClientManager.cs | 18 ---------------- .../LiveTv/Listings/SchedulesDirect.cs | 10 +-------- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 2 -- .../LiveTv/TunerHosts/SharedHttpStream.cs | 6 ------ MediaBrowser.Common/Net/HttpRequestOptions.cs | 21 +------------------ 6 files changed, 7 insertions(+), 61 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f7d9bad1b0..2f396f8146 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; @@ -34,7 +35,6 @@ using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; -using Emby.Server.Implementations.Middleware; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Reflection; @@ -1479,12 +1479,12 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = false, LogRequest = false, - TimeoutMs = 10000, BufferContent = false, CancellationToken = cancellationToken })) { - return GetLocalApiUrl(response.ReadToEnd().Trim()); + string res = await response.ReadToEndAsync().ConfigureAwait(false); + return GetLocalApiUrl(res.Trim()); } } catch (Exception ex) @@ -1604,16 +1604,15 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = logPing, LogRequest = logPing, - TimeoutMs = 5000, BufferContent = false, CancellationToken = cancellationToken - }, "POST").ConfigureAwait(false)) + }, HttpMethod.Post).ConfigureAwait(false)) { using (var reader = new StreamReader(response.Content)) { - var result = reader.ReadToEnd(); + var result = await reader.ReadToEndAsync().ConfigureAwait(false); var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 581d6bafd1..b82d55d0e6 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -352,11 +352,6 @@ namespace Emby.Server.Implementations.HttpClientManager } } - if (options.ResourcePool != null) - { - await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); - } - if (options.LogRequest) { _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); @@ -397,10 +392,6 @@ namespace Emby.Server.Implementations.HttpClientManager { throw GetCancellationException(options, options.CancellationToken, ex); } - finally - { - options.ResourcePool?.Release(); - } } private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, Stream content, long? contentLength, IDisposable disposable) @@ -483,11 +474,6 @@ namespace Emby.Server.Implementations.HttpClientManager var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); - if (options.ResourcePool != null) - { - await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); - } - options.Progress.Report(0); if (options.LogRequest) @@ -528,10 +514,6 @@ namespace Emby.Server.Implementations.HttpClientManager throw GetException(ex, options); } - finally - { - options.ResourcePool?.Release(); - } } private Exception GetException(Exception ex, HttpRequestOptions options) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 4137760d07..f3f7477180 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -96,8 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/schedules", UserAgent = UserAgent, CancellationToken = cancellationToken, - // The data can be large so give it some extra time - TimeoutMs = 60000, LogErrorResponseBody = true, RequestContent = requestString }; @@ -115,9 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/programs", UserAgent = UserAgent, CancellationToken = cancellationToken, - LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; @@ -483,8 +479,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings CancellationToken = cancellationToken, RequestContent = imageIdString, LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 }; try @@ -871,8 +865,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings UserAgent = UserAgent, CancellationToken = cancellationToken, LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 }; httpOptions.RequestHeaders["token"] = token; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 24b100eddc..761275f8f3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds), BufferContent = false }, "GET").ConfigureAwait(false)) @@ -191,7 +190,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/tuners.html", GetApiUrl(info)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), BufferContent = false })) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index d74cf3be2d..e8b34da0ca 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -47,13 +47,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts CancellationToken = CancellationToken.None, BufferContent = false, - // Increase a little bit - TimeoutMs = 30000, - EnableHttpCompression = false, - - LogResponse = true, - LogResponseHeaders = true }; foreach (var header in mediaSource.RequiredHttpHeaders) diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 874383fedf..38e0ff0f55 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -28,18 +28,13 @@ namespace MediaBrowser.Common.Net get => GetHeaderValue(HeaderNames.Accept); set => RequestHeaders[HeaderNames.Accept] = value; } + /// /// Gets or sets the cancellation token. /// /// The cancellation token. public CancellationToken CancellationToken { get; set; } - /// - /// Gets or sets the resource pool. - /// - /// The resource pool. - public SemaphoreSlim ResourcePool { get; set; } - /// /// Gets or sets the user agent. /// @@ -86,8 +81,6 @@ namespace MediaBrowser.Common.Net public bool LogRequest { get; set; } public bool LogRequestAsDebug { get; set; } public bool LogErrors { get; set; } - public bool LogResponse { get; set; } - public bool LogResponseHeaders { get; set; } public bool LogErrorResponseBody { get; set; } public bool EnableKeepAlive { get; set; } @@ -95,7 +88,6 @@ namespace MediaBrowser.Common.Net public CacheMode CacheMode { get; set; } public TimeSpan CacheLength { get; set; } - public int TimeoutMs { get; set; } public bool EnableDefaultUserAgent { get; set; } public bool AppendCharsetToMimeType { get; set; } @@ -119,17 +111,6 @@ namespace MediaBrowser.Common.Net LogRequest = true; LogErrors = true; CacheMode = CacheMode.None; - - TimeoutMs = 20000; - } - - public void SetPostData(IDictionary values) - { - var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key])); - var postContent = string.Join("&", strings.ToArray()); - - RequestContent = postContent; - RequestContentType = "application/x-www-form-urlencoded"; } } From f2062ba19bb38a2dfa6b10ab778718730fe0f14a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Fri, 8 Mar 2019 22:25:45 +0100 Subject: [PATCH 0194/6208] Disable HTTPS in Kestrel if Certificate is null --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f7d9bad1b0..ae54606747 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -629,7 +629,7 @@ namespace Emby.Server.Implementations { options.ListenAnyIP(HttpPort); - if (EnableHttps) + if (EnableHttps && Certificate != null) { options.ListenAnyIP(HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); } From 47095e6cf87d73aad2abf2f9f326631cf95704fa Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 9 Mar 2019 11:17:02 +0900 Subject: [PATCH 0195/6208] move a variable out of for loop --- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index f2162432a9..7d404ce644 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -77,12 +77,14 @@ namespace Jellyfin.Drawing.Skia { canvas.Clear(SKColors.Black); + // number of images used in the thumbnail + var iCount = 3; + // determine sizes for each image that will composited into the final image - var iSlice = Convert.ToInt32(width * 0.33); - int iTrans = Convert.ToInt32(height * 0.25); + var iSlice = Convert.ToInt32(width / iCount); int iHeight = Convert.ToInt32(height * 1.00); int imageIndex = 0; - for (int i = 0; i < 3; i++) + for (int i = 0; i < iCount; i++) { using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) { From 93fe7957fb4ed4f85b14bc4560f8e595540e91bd Mon Sep 17 00:00:00 2001 From: Matsuri Date: Fri, 1 Mar 2019 05:50:39 +0000 Subject: [PATCH 0196/6208] Translated using Weblate (Chinese (Simplified)) Currently translated at 97.8% (92 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 8910a6bce8..6f7d362d3b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -34,14 +34,14 @@ "LabelRunningTimeValue": "运行时间:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 服务器已更新", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server 的版本已更新为 {0}", "MessageNamedServerConfigurationUpdatedWithValue": "服务器配置 {0} 部分已更新", "MessageServerConfigurationUpdated": "服务器配置已更新", "MixedContent": "混合内容", "Movies": "电影", "Music": "音乐", "MusicVideos": "音乐视频", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "{0} 安装失败", "NameSeasonNumber": "季 {0}", "NameSeasonUnknown": "未知季", "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", @@ -70,7 +70,7 @@ "ProviderValue": "提供商:{0}", "ScheduledTaskFailedWithName": "{0} 已失败", "ScheduledTaskStartedWithName": "{0} 已开始", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} 需要重新启动", "Shows": "节目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin 服务器加载中。请稍后再试。", From 16adaa64c9c94ea35ffd99df114a0b8de9249cf6 Mon Sep 17 00:00:00 2001 From: WWWesten Date: Mon, 4 Mar 2019 12:00:32 +0000 Subject: [PATCH 0197/6208] Translated using Weblate (Kazakh) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kk/ --- Emby.Server.Implementations/Localization/Core/kk.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 658d168e9f..23841f37d6 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -3,15 +3,15 @@ "AppDeviceValues": "Qoldanba: {0}, Qurylǵy: {1}", "Application": "Qoldanba", "Artists": "Oryndaýshylar", - "AuthenticationSucceededWithUserName": "{0} túpnusqalyǵyn rastalýy sátti", + "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy", "Books": "Kitaptar", - "CameraImageUploadedFrom": "Jańa sýret {0} kamerasynan júktep alyndy", + "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy", "Channels": "Arnalar", "ChapterNameValue": "{0}-sahna", "Collections": "Jıyntyqtar", "DeviceOfflineWithName": "{0} ajyratylǵan", "DeviceOnlineWithName": "{0} qosylǵan", - "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz", + "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz aıaqtaldy", "Favorites": "Tańdaýlylar", "Folders": "Qaltalar", "Genres": "Janrlar", @@ -28,13 +28,13 @@ "HeaderRecordingGroups": "Jazba toptary", "HomeVideos": "Úılik beıneler", "Inherit": "Muraǵa ıelený", - "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústelindi", + "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústeldi", "ItemRemovedWithName": "{0} tasyǵyshhanadan alastaldy", "LabelIpAddressValue": "IP-mekenjaıy: {0}", "LabelRunningTimeValue": "Oınatý ýaqyty: {0}", "Latest": "Eń keıingi", "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy", - "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} deńgeıge jańartyldy", + "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy", "MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy", "MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy", "MixedContent": "Aralas mazmun", From 93d15cd96972588bbbc29a9398c9d4b050d30411 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 16:17:48 -0400 Subject: [PATCH 0198/6208] Add configuration flag for Web directory --- .../AppBase/BaseApplicationPaths.cs | 4 +++- .../ServerApplicationPaths.cs | 6 ++++-- Jellyfin.Server/Program.cs | 19 ++++++++++++++++++- Jellyfin.Server/StartupOptions.cs | 3 +++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 65cdccfa5d..3ee09c9c04 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -17,12 +17,14 @@ namespace Emby.Server.Implementations.AppBase string programDataPath, string logDirectoryPath, string configurationDirectoryPath, - string cacheDirectoryPath) + string cacheDirectoryPath, + string webDirectoryPath) { ProgramDataPath = programDataPath; LogDirectoryPath = logDirectoryPath; ConfigurationDirectoryPath = configurationDirectoryPath; CachePath = cacheDirectoryPath; + WebPath = webDirectoryPath; DataPath = Path.Combine(ProgramDataPath, "data"); } diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 05f6469ece..adaf23234f 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -17,11 +17,13 @@ namespace Emby.Server.Implementations string programDataPath, string logDirectoryPath, string configurationDirectoryPath, - string cacheDirectoryPath) + string cacheDirectoryPath, + string webDirectoryPath) : base(programDataPath, logDirectoryPath, configurationDirectoryPath, - cacheDirectoryPath) + cacheDirectoryPath, + webDirectoryPath) { } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0ef1711d4c..3d81ade35c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -264,6 +264,23 @@ namespace Jellyfin.Server } } + // webDir + // IF --webdir + // ELSE IF $JELLYFIN_WEB_DIR + // ELSE use /jellyfin-web + var webDir = options.WebDir; + + if (string.IsNullOrEmpty(webDir)) + { + webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); + + if (string.IsNullOrEmpty(webDir)) + { + // Use default location under ResourcesPath + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web") + } + } + // logDir // IF --logdir // ELSE IF $JELLYFIN_LOG_DIR @@ -296,7 +313,7 @@ namespace Jellyfin.Server Environment.Exit(1); } - return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir); + return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); } private static async Task CreateConfiguration(IApplicationPaths appPaths) diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index c8cdb984db..2b01c7bbdf 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -11,6 +11,9 @@ namespace Jellyfin.Server [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] public string DataDir { get; set; } + [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web resources.")] + public string WebDir { get; set; } + [Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")] public string CacheDir { get; set; } From 25deca95793581df85dab8c454d53a1d07a6ab53 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 16:20:46 -0400 Subject: [PATCH 0199/6208] Make use of WebPath --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- MediaBrowser.WebDashboard/Api/DashboardService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ae54606747..05ede0ddab 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -621,7 +621,7 @@ namespace Emby.Server.Implementations string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; if (string.IsNullOrEmpty(contentRoot)) { - contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src"); + contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths,WebPath, "src"); } var host = new WebHostBuilder() diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 531978e1dd..05df54b115 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -149,7 +149,7 @@ namespace MediaBrowser.WebDashboard.Api return _serverConfigurationManager.Configuration.DashboardSourcePath; } - return Path.Combine(_serverConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src"); + return Path.Combine(_serverConfigurationManager.ApplicationPaths.WebPath, "src"); } } From 132ce3ece14ff7ca756c81e0a12172744c5c50aa Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 17:04:18 -0400 Subject: [PATCH 0200/6208] Add further resources to complete WebPath --- Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 6 ++++++ Emby.Server.Implementations/ApplicationHost.cs | 4 +++- Emby.Server.Implementations/ServerApplicationPaths.cs | 6 ++++++ Jellyfin.Server/Program.cs | 2 +- MediaBrowser.Common/Configuration/IApplicationPaths.cs | 6 ++++++ MediaBrowser.Model/System/SystemInfo.cs | 6 ++++++ 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 3ee09c9c04..fd9ce3a364 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -35,6 +35,12 @@ namespace Emby.Server.Implementations.AppBase /// The program data path. public string ProgramDataPath { get; private set; } + /// + /// Gets the path to the web resources folder + /// + /// The web resources path. + public string WebPath { get; set; } + /// /// Gets the path to the system folder /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 05ede0ddab..f240ef871a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -621,7 +621,7 @@ namespace Emby.Server.Implementations string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; if (string.IsNullOrEmpty(contentRoot)) { - contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths,WebPath, "src"); + contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.WebPath, "src"); } var host = new WebHostBuilder() @@ -921,6 +921,7 @@ namespace Emby.Server.Implementations logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount); logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath); + logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath); logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); } @@ -1393,6 +1394,7 @@ namespace Emby.Server.Implementations CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), Id = SystemId, ProgramDataPath = ApplicationPaths.ProgramDataPath, + WebPath = ApplicationPaths.WebPath, LogPath = ApplicationPaths.LogDirectoryPath, ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index adaf23234f..68d5881220 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -35,6 +35,12 @@ namespace Emby.Server.Implementations /// The root folder path. public string RootFolderPath => Path.Combine(ProgramDataPath, "root"); +/* /// + /// Gets the path to the Web resources + /// + /// The web folder path. + public string WebPath => WebPath; */ + /// /// Gets the path to the default user view directory. Used if no specific user view is defined. /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3d81ade35c..8f1498b13b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -277,7 +277,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(webDir)) { // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web") + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); } } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index cb4e8bf5f0..1f56c98fd8 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -11,6 +11,12 @@ namespace MediaBrowser.Common.Configuration /// The program data path. string ProgramDataPath { get; } + /// + /// Gets the path to the web resources folder + /// + /// The web resources path. + string WebPath { get; } + /// /// Gets the path to the program system folder /// diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 6482f2c840..fa72624819 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -83,6 +83,12 @@ namespace MediaBrowser.Model.System /// The program data path. public string ProgramDataPath { get; set; } + /// + /// Gets or sets the web resources path. + /// + /// The web resources path. + public string WebPath { get; set; } + /// /// Gets or sets the items by name path. /// From ecf85a73ec4aa918dc9a816dc60e6e5d76a36ecd Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 17:09:51 -0400 Subject: [PATCH 0201/6208] Use environment variables instead of opts for Deb --- deployment/debian-package-x64/pkg-src/conf/jellyfin | 10 +++++----- deployment/debian-package-x64/pkg-src/jellyfin.service | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index 58fe79332a..7db482d428 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -13,10 +13,10 @@ # # Program directories -JELLYFIN_DATA_DIRECTORY="/var/lib/jellyfin" -JELLYFIN_CONFIG_DIRECTORY="/etc/jellyfin" -JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin" -JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" +JELLYFIN_DATA_DIR="/var/lib/jellyfin" +JELLYFIN_CONFIG_DIR="/etc/jellyfin" +JELLYFIN_LOG_DIR="/var/log/jellyfin" +JELLYFIN_CACHE_DIR="/var/cache/jellyfin" # Restart script for in-app server control JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" @@ -38,4 +38,4 @@ JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="--datadir=$JELLYFIN_DATA_DIRECTORY --configdir=$JELLYFIN_CONFIG_DIRECTORY --logdir=$JELLYFIN_LOG_DIRECTORY --cachedir=$JELLYFIN_CACHE_DIRECTORY $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" +JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.service b/deployment/debian-package-x64/pkg-src/jellyfin.service index 9c6c6667f1..b4da3a945b 100644 --- a/deployment/debian-package-x64/pkg-src/jellyfin.service +++ b/deployment/debian-package-x64/pkg-src/jellyfin.service @@ -6,7 +6,7 @@ After = network.target Type = simple EnvironmentFile = /etc/default/jellyfin User = jellyfin -ExecStart = /usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} Restart = on-failure TimeoutSec = 15 From 86f5221f96b09bde177c881eb34cad0ec10cbe90 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 17:11:16 -0400 Subject: [PATCH 0202/6208] Use environment variables instead of opts for RPM --- deployment/fedora-package-x64/pkg-src/jellyfin.env | 8 ++++---- deployment/fedora-package-x64/pkg-src/jellyfin.service | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.env b/deployment/fedora-package-x64/pkg-src/jellyfin.env index abfa670e4a..143a317c43 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.env +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.env @@ -15,10 +15,10 @@ # # Program directories -JELLYFIN_DATA_DIRECTORY="/var/lib/jellyfin" -JELLYFIN_CONFIG_DIRECTORY="/etc/jellyfin" -JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin" -JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" +JELLYFIN_DATA_DIR="/var/lib/jellyfin" +JELLYFIN_CONFIG_DIR="/etc/jellyfin" +JELLYFIN_LOG_DIR="/var/log/jellyfin" +JELLYFIN_CACHE_DIR="/var/cache/jellyfin" # In-App service control JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.service b/deployment/fedora-package-x64/pkg-src/jellyfin.service index d58df9d949..1f83d3d386 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.service +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.service @@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of [Service] EnvironmentFile=/etc/sysconfig/jellyfin WorkingDirectory=/var/lib/jellyfin -ExecStart=/usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} TimeoutSec=15 Restart=on-failure User=jellyfin From 5f7524aca26797a3dc7acbed8cd50e113e2f9a4d Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 17:21:10 -0400 Subject: [PATCH 0203/6208] Remove unneccessary string --- Emby.Server.Implementations/ServerApplicationPaths.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 68d5881220..adaf23234f 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -35,12 +35,6 @@ namespace Emby.Server.Implementations /// The root folder path. public string RootFolderPath => Path.Combine(ProgramDataPath, "root"); -/* /// - /// Gets the path to the Web resources - /// - /// The web folder path. - public string WebPath => WebPath; */ - /// /// Gets the path to the default user view directory. Used if no specific user view is defined. /// From 5268553e7fae2975ede06a944944f3b508453a1a Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 18:24:11 -0400 Subject: [PATCH 0204/6208] Have datadir envvar match the others --- Jellyfin.Server/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0ef1711d4c..c2268c8025 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -174,7 +174,7 @@ namespace Jellyfin.Server { // dataDir // IF --datadir - // ELSE IF $JELLYFIN_DATA_PATH + // ELSE IF $JELLYFIN_DATA_DIR // ELSE IF windows, use <%APPDATA%>/jellyfin // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin // ELSE use $HOME/.local/share/jellyfin @@ -182,7 +182,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(dataDir)) { - dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH"); + dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); if (string.IsNullOrEmpty(dataDir)) { From 037cf9e1eeb4e500b4bdc379c76fe5a5922c7f54 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 10 Mar 2019 18:30:10 -0400 Subject: [PATCH 0205/6208] Move CreateDirectory for dataDir to try block --- Jellyfin.Server/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c2268c8025..fa4b7b8e52 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -191,8 +191,6 @@ namespace Jellyfin.Server } } - Directory.CreateDirectory(dataDir); - // configDir // IF --configdir // ELSE IF $JELLYFIN_CONFIG_DIR @@ -285,6 +283,7 @@ namespace Jellyfin.Server // Ensure the main folders exist before we continue try { + Directory.CreateDirectory(dataDir); Directory.CreateDirectory(logDir); Directory.CreateDirectory(configDir); Directory.CreateDirectory(cacheDir); From 7322485a6d264037f2e26bf40bac93d6598e7bff Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 11 Mar 2019 20:44:12 +0100 Subject: [PATCH 0206/6208] Skip processing of images that don't exist --- Emby.Drawing/ImageProcessor.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index faaeb5af84..6d209d8d01 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -180,6 +180,12 @@ namespace Emby.Drawing var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false); originalImagePath = supportedImageInfo.path; + + if (!File.Exists(originalImagePath)) + { + return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); + } + dateModified = supportedImageInfo.dateModified; bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath)); @@ -265,8 +271,6 @@ namespace Emby.Drawing { // If it fails for whatever reason, return the original image _logger.LogError(ex, "Error encoding image"); - - // Just spit out the original file if all the options are default return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } finally From 0ff038f0a2c90ba0ad671eae1ac3c74a7602b855 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 11 Mar 2019 23:13:01 +0100 Subject: [PATCH 0207/6208] Fix nullref --- Emby.Server.Implementations/ApplicationHost.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ae54606747..d7696393db 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -700,6 +700,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ApplicationPaths); + serviceCollection.AddSingleton(_configuration); + serviceCollection.AddSingleton(JsonSerializer); serviceCollection.AddSingleton(LoggerFactory); From 715ddbb3b08cab2ff64919f577d5f72d9af5ea22 Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 11 Mar 2019 18:10:31 -0700 Subject: [PATCH 0208/6208] remove open subtitles from the server --- .../Emby.Server.Implementations.csproj | 1 - .../MediaBrowser.MediaEncoding.csproj | 1 - .../Subtitles/ConfigurationExtension.cs | 29 - .../Subtitles/OpenSubtitleDownloader.cs | 347 --- MediaBrowser.sln | 2 - OpenSubtitlesHandler/Console/OSHConsole.cs | 92 - .../Interfaces/IMethodResponse.cs | 71 - .../Interfaces/MethodResponseAttr.cs | 40 - .../Languages/DetectLanguageResult.cs | 31 - .../MethodResponseAddComment.cs | 32 - .../MethodResponseAddRequest.cs | 35 - .../MethodResponseAutoUpdate.cs | 59 - .../MethodResponseCheckMovieHash.cs | 37 - .../MethodResponseCheckMovieHash2.cs | 37 - .../MethodResponseCheckSubHash.cs | 38 - .../MethodResponseDetectLanguage.cs | 37 - .../MethodResponses/MethodResponseError.cs | 36 - .../MethodResponseGetAvailableTranslations.cs | 37 - .../MethodResponseGetComments.cs | 39 - .../MethodResponseGetSubLanguages.cs | 39 - .../MethodResponseGetTranslation.cs | 36 - .../MethodResponseInsertMovie.cs | 36 - .../MethodResponseInsertMovieHash.cs | 38 - .../MethodResponses/MethodResponseLogIn.cs | 39 - .../MethodResponseMovieDetails.cs | 73 - .../MethodResponseMovieSearch.cs | 43 - .../MethodResponseNoOperation.cs | 52 - .../MethodResponseReportWrongImdbMovie.cs | 33 - .../MethodResponseReportWrongMovieHash.cs | 33 - .../MethodResponseSearchToMail.cs | 32 - .../MethodResponseServerInfo.cs | 131 - .../MethodResponseSubtitleDownload.cs | 44 - .../MethodResponseSubtitleSearch.cs | 46 - .../MethodResponseSubtitlesVote.cs | 43 - .../MethodResponseTryUploadSubtitles.cs | 40 - .../MethodResponseUploadSubtitles.cs | 38 - OpenSubtitlesHandler/Methods Implemeted.txt | 39 - OpenSubtitlesHandler/MovieHasher.cs | 48 - .../Movies/CheckMovieHash2Data.cs | 42 - .../Movies/CheckMovieHash2Result.cs | 37 - .../Movies/CheckMovieHashResult.cs | 41 - .../Movies/InsertMovieHashParameters.cs | 38 - .../Movies/MovieSearchResult.cs | 40 - .../Movies/SearchToMailMovieParameter.cs | 30 - OpenSubtitlesHandler/OpenSubtitles.cs | 2744 ----------------- .../OpenSubtitlesHandler.csproj | 13 - .../GetAvailableTranslationsResult.cs | 39 - .../OtherTypes/GetCommentsResult.cs | 36 - .../Properties/AssemblyInfo.cs | 24 - OpenSubtitlesHandler/Readme.txt | 20 - .../SubtitleTypes/CheckSubHashResult.cs | 30 - .../SubtitleTypes/SubtitleDownloadResult.cs | 43 - .../SubtitleTypes/SubtitleLanguage.cs | 39 - .../SubtitleTypes/SubtitleSearchParameters.cs | 82 - .../SubtitleTypes/SubtitleSearchResult.cs | 136 - .../TryUploadSubtitlesParameters.cs | 47 - .../UploadSubtitleInfoParameter.cs | 46 - .../SubtitleTypes/UploadSubtitleParameters.cs | 52 - OpenSubtitlesHandler/Utilities.cs | 174 -- OpenSubtitlesHandler/XML-RPC/Docs/XML-RPC.txt | 225 -- .../XML-RPC/Enums/XmlRpcValueType.cs | 25 - .../XML-RPC/Types/XmlRpcMethodCall.cs | 63 - .../XML-RPC/Values/IXmlRpcValue.cs | 36 - .../XML-RPC/Values/XmlRpcStructMember.cs | 43 - .../XML-RPC/Values/XmlRpcValueArray.cs | 121 - .../XML-RPC/Values/XmlRpcValueBasic.cs | 99 - .../XML-RPC/Values/XmlRpcValueStruct.cs | 43 - .../XML-RPC/XmlRpcGenerator.cs | 350 --- 68 files changed, 6642 deletions(-) delete mode 100644 MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs delete mode 100644 MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs delete mode 100644 OpenSubtitlesHandler/Console/OSHConsole.cs delete mode 100644 OpenSubtitlesHandler/Interfaces/IMethodResponse.cs delete mode 100644 OpenSubtitlesHandler/Interfaces/MethodResponseAttr.cs delete mode 100644 OpenSubtitlesHandler/Languages/DetectLanguageResult.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseAddComment.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseAddRequest.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseAutoUpdate.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash2.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseCheckSubHash.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseDetectLanguage.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseError.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseGetAvailableTranslations.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseGetComments.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseGetSubLanguages.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseGetTranslation.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovie.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovieHash.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseLogIn.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseMovieDetails.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseMovieSearch.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseNoOperation.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongImdbMovie.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongMovieHash.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseSearchToMail.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseServerInfo.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleDownload.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleSearch.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitlesVote.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseTryUploadSubtitles.cs delete mode 100644 OpenSubtitlesHandler/MethodResponses/MethodResponseUploadSubtitles.cs delete mode 100644 OpenSubtitlesHandler/Methods Implemeted.txt delete mode 100644 OpenSubtitlesHandler/MovieHasher.cs delete mode 100644 OpenSubtitlesHandler/Movies/CheckMovieHash2Data.cs delete mode 100644 OpenSubtitlesHandler/Movies/CheckMovieHash2Result.cs delete mode 100644 OpenSubtitlesHandler/Movies/CheckMovieHashResult.cs delete mode 100644 OpenSubtitlesHandler/Movies/InsertMovieHashParameters.cs delete mode 100644 OpenSubtitlesHandler/Movies/MovieSearchResult.cs delete mode 100644 OpenSubtitlesHandler/Movies/SearchToMailMovieParameter.cs delete mode 100644 OpenSubtitlesHandler/OpenSubtitles.cs delete mode 100644 OpenSubtitlesHandler/OpenSubtitlesHandler.csproj delete mode 100644 OpenSubtitlesHandler/OtherTypes/GetAvailableTranslationsResult.cs delete mode 100644 OpenSubtitlesHandler/OtherTypes/GetCommentsResult.cs delete mode 100644 OpenSubtitlesHandler/Properties/AssemblyInfo.cs delete mode 100644 OpenSubtitlesHandler/Readme.txt delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/CheckSubHashResult.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/SubtitleDownloadResult.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/SubtitleLanguage.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchParameters.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchResult.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/TryUploadSubtitlesParameters.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleInfoParameter.cs delete mode 100644 OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleParameters.cs delete mode 100644 OpenSubtitlesHandler/Utilities.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Docs/XML-RPC.txt delete mode 100644 OpenSubtitlesHandler/XML-RPC/Enums/XmlRpcValueType.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Types/XmlRpcMethodCall.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Values/IXmlRpcValue.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Values/XmlRpcStructMember.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueArray.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueBasic.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueStruct.cs delete mode 100644 OpenSubtitlesHandler/XML-RPC/XmlRpcGenerator.cs diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 39e9ed3756..0b0dab2139 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,6 @@ - diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 68b8bd4fae..e4757543ea 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -14,7 +14,6 @@ - diff --git a/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs b/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs deleted file mode 100644 index 92544f4f62..0000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Providers; - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - public static class ConfigurationExtension - { - public static SubtitleOptions GetSubtitleConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration("subtitles"); - } - } - - public class SubtitleConfigurationFactory : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new List - { - new ConfigurationStore - { - Key = "subtitles", - ConfigurationType = typeof (SubtitleOptions) - } - }; - } - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs deleted file mode 100644 index a7e3f61972..0000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs +++ /dev/null @@ -1,347 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using OpenSubtitlesHandler; - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - public class OpenSubtitleDownloader : ISubtitleProvider, IDisposable - { - private readonly ILogger _logger; - private readonly IHttpClient _httpClient; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private readonly IServerConfigurationManager _config; - - private readonly IJsonSerializer _json; - private readonly IFileSystem _fileSystem; - - public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem) - { - _logger = loggerFactory.CreateLogger(GetType().Name); - _httpClient = httpClient; - _config = config; - _json = json; - _fileSystem = fileSystem; - - _config.NamedConfigurationUpdating += _config_NamedConfigurationUpdating; - - Utilities.HttpClient = httpClient; - OpenSubtitles.SetUserAgent("jellyfin"); - } - - private const string PasswordHashPrefix = "h:"; - void _config_NamedConfigurationUpdating(object sender, ConfigurationUpdateEventArgs e) - { - if (!string.Equals(e.Key, "subtitles", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var options = (SubtitleOptions)e.NewConfiguration; - - if (options != null && - !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) && - !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) - { - options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash); - } - } - - private static string EncodePassword(string password) - { - var bytes = Encoding.UTF8.GetBytes(password); - return PasswordHashPrefix + Convert.ToBase64String(bytes); - } - - private static string DecodePassword(string password) - { - if (password == null || - !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var bytes = Convert.FromBase64String(password.Substring(2)); - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); - } - - public string Name => "Open Subtitles"; - - private SubtitleOptions GetOptions() - { - return _config.GetSubtitleConfiguration(); - } - - public IEnumerable SupportedMediaTypes - { - get - { - var options = GetOptions(); - - if (string.IsNullOrWhiteSpace(options.OpenSubtitlesUsername) || - string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash)) - { - return new VideoContentType[] { }; - } - - return new[] { VideoContentType.Episode, VideoContentType.Movie }; - } - } - - public Task GetSubtitles(string id, CancellationToken cancellationToken) - { - return GetSubtitlesInternal(id, GetOptions(), cancellationToken); - } - - private DateTime _lastRateLimitException; - private async Task GetSubtitlesInternal(string id, - SubtitleOptions options, - CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException(nameof(id)); - } - - var idParts = id.Split(new[] { '-' }, 3); - - var format = idParts[0]; - var language = idParts[1]; - var ossId = idParts[2]; - - var downloadsList = new[] { int.Parse(ossId, _usCulture) }; - - await Login(cancellationToken).ConfigureAwait(false); - - if ((DateTime.UtcNow - _lastRateLimitException).TotalHours < 1) - { - throw new RateLimitExceededException("OpenSubtitles rate limit reached"); - } - - var resultDownLoad = await OpenSubtitles.DownloadSubtitlesAsync(downloadsList, cancellationToken).ConfigureAwait(false); - - if ((resultDownLoad.Status ?? string.Empty).IndexOf("407", StringComparison.OrdinalIgnoreCase) != -1) - { - _lastRateLimitException = DateTime.UtcNow; - throw new RateLimitExceededException("OpenSubtitles rate limit reached"); - } - - if (!(resultDownLoad is MethodResponseSubtitleDownload)) - { - throw new Exception("Invalid response type"); - } - - var results = ((MethodResponseSubtitleDownload)resultDownLoad).Results; - - _lastRateLimitException = DateTime.MinValue; - - if (results.Count == 0) - { - var msg = string.Format("Subtitle with Id {0} was not found. Name: {1}. Status: {2}. Message: {3}", - ossId, - resultDownLoad.Name ?? string.Empty, - resultDownLoad.Status ?? string.Empty, - resultDownLoad.Message ?? string.Empty); - - throw new ResourceNotFoundException(msg); - } - - var data = Convert.FromBase64String(results.First().Data); - - return new SubtitleResponse - { - Format = format, - Language = language, - - Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data))) - }; - } - - private DateTime _lastLogin; - private async Task Login(CancellationToken cancellationToken) - { - if ((DateTime.UtcNow - _lastLogin).TotalSeconds < 60) - { - return; - } - - var options = GetOptions(); - - var user = options.OpenSubtitlesUsername ?? string.Empty; - var password = DecodePassword(options.OpenSubtitlesPasswordHash); - - var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false); - - if (!(loginResponse is MethodResponseLogIn)) - { - throw new Exception("Authentication to OpenSubtitles failed."); - } - - _lastLogin = DateTime.UtcNow; - } - - public async Task> GetSupportedLanguages(CancellationToken cancellationToken) - { - await Login(cancellationToken).ConfigureAwait(false); - - var result = OpenSubtitles.GetSubLanguages("en"); - if (!(result is MethodResponseGetSubLanguages)) - { - _logger.LogError("Invalid response type"); - return new List(); - } - - var results = ((MethodResponseGetSubLanguages)result).Languages; - - return results.Select(i => new NameIdPair - { - Name = i.LanguageName, - Id = i.SubLanguageID - }); - } - - private string NormalizeLanguage(string language) - { - // Problem with Greek subtitle download #1349 - if (string.Equals(language, "gre", StringComparison.OrdinalIgnoreCase)) - { - - return "ell"; - } - - return language; - } - - public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) - { - var imdbIdText = request.GetProviderId(MetadataProviders.Imdb); - long imdbId = 0; - - switch (request.ContentType) - { - case VideoContentType.Episode: - if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName)) - { - _logger.LogDebug("Episode information missing"); - return new List(); - } - break; - case VideoContentType.Movie: - if (string.IsNullOrEmpty(request.Name)) - { - _logger.LogDebug("Movie name missing"); - return new List(); - } - if (string.IsNullOrWhiteSpace(imdbIdText) || !long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId)) - { - _logger.LogDebug("Imdb id missing"); - return new List(); - } - break; - } - - if (string.IsNullOrEmpty(request.MediaPath)) - { - _logger.LogDebug("Path Missing"); - return new List(); - } - - await Login(cancellationToken).ConfigureAwait(false); - - var subLanguageId = NormalizeLanguage(request.Language); - string hash; - - using (var fileStream = File.OpenRead(request.MediaPath)) - { - hash = Utilities.ComputeHash(fileStream); - } - var fileInfo = _fileSystem.GetFileInfo(request.MediaPath); - var movieByteSize = fileInfo.Length; - var searchImdbId = request.ContentType == VideoContentType.Movie ? imdbId.ToString(_usCulture) : ""; - var subtitleSearchParameters = request.ContentType == VideoContentType.Episode - ? new List { - new SubtitleSearchParameters(subLanguageId, - query: request.SeriesName, - season: request.ParentIndexNumber.Value.ToString(_usCulture), - episode: request.IndexNumber.Value.ToString(_usCulture)) - } - : new List { - new SubtitleSearchParameters(subLanguageId, imdbid: searchImdbId), - new SubtitleSearchParameters(subLanguageId, query: request.Name, imdbid: searchImdbId) - }; - var parms = new List { - new SubtitleSearchParameters( subLanguageId, - movieHash: hash, - movieByteSize: movieByteSize, - imdbid: searchImdbId ), - }; - parms.AddRange(subtitleSearchParameters); - var result = await OpenSubtitles.SearchSubtitlesAsync(parms.ToArray(), cancellationToken).ConfigureAwait(false); - if (!(result is MethodResponseSubtitleSearch)) - { - _logger.LogError("Invalid response type"); - return new List(); - } - - Predicate mediaFilter = - x => - request.ContentType == VideoContentType.Episode - ? !string.IsNullOrEmpty(x.SeriesSeason) && !string.IsNullOrEmpty(x.SeriesEpisode) && - int.Parse(x.SeriesSeason, _usCulture) == request.ParentIndexNumber && - int.Parse(x.SeriesEpisode, _usCulture) == request.IndexNumber - : !string.IsNullOrEmpty(x.IDMovieImdb) && long.Parse(x.IDMovieImdb, _usCulture) == imdbId; - - var results = ((MethodResponseSubtitleSearch)result).Results; - - // Avoid implicitly captured closure - var hasCopy = hash; - - return results.Where(x => x.SubBad == "0" && mediaFilter(x) && (!request.IsPerfectMatch || string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase))) - .OrderBy(x => (string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase) ? 0 : 1)) - .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize, _usCulture) - movieByteSize)) - .ThenByDescending(x => int.Parse(x.SubDownloadsCnt, _usCulture)) - .ThenByDescending(x => double.Parse(x.SubRating, _usCulture)) - .Select(i => new RemoteSubtitleInfo - { - Author = i.UserNickName, - Comment = i.SubAuthorComment, - CommunityRating = float.Parse(i.SubRating, _usCulture), - DownloadCount = int.Parse(i.SubDownloadsCnt, _usCulture), - Format = i.SubFormat, - ProviderName = Name, - ThreeLetterISOLanguageName = i.SubLanguageID, - - Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitleFile, - - Name = i.SubFileName, - DateCreated = DateTime.Parse(i.SubAddDate, _usCulture), - IsHashMatch = i.MovieHash == hasCopy - - }).Where(i => !string.Equals(i.Format, "sub", StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Format, "idx", StringComparison.OrdinalIgnoreCase)); - } - - public void Dispose() - { - _config.NamedConfigurationUpdating -= _config_NamedConfigurationUpdating; - } - } -} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index a3c0569bad..3ed86d65c7 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" diff --git a/OpenSubtitlesHandler/Console/OSHConsole.cs b/OpenSubtitlesHandler/Console/OSHConsole.cs deleted file mode 100644 index 396b28cbc6..0000000000 --- a/OpenSubtitlesHandler/Console/OSHConsole.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System; - -namespace OpenSubtitlesHandler.Console -{ - public class OSHConsole - { - /// - /// Write line to the console and raise the "LineWritten" event - /// - /// - /// The debug line - /// The status - public static void WriteLine(string text, DebugCode code = DebugCode.None) - { - if (LineWritten != null) - LineWritten(null, new DebugEventArgs(text, code)); - } - /// - /// Update the last written line - /// - /// The debug line - /// The status - public static void UpdateLine(string text, DebugCode code = DebugCode.None) - { - if (UpdateLastLine != null) - UpdateLastLine(null, new DebugEventArgs(text, code)); - } - - public static event EventHandler LineWritten; - public static event EventHandler UpdateLastLine; - } - public enum DebugCode - { - None, - Good, - Warning, - Error - } - /// - /// Console Debug Args - /// - public class DebugEventArgs : EventArgs - { - public DebugCode Code { get; private set; } - public string Text { get; private set; } - - /// - /// Console Debug Args - /// - /// The debug line - /// The status - public DebugEventArgs(string text, DebugCode code) - { - this.Text = text; - this.Code = code; - } - } - public struct DebugLine - { - public DebugLine(string debugLine, DebugCode status) - { - this.debugLine = debugLine; - this.status = status; - } - string debugLine; - DebugCode status; - - public string Text - { get { return debugLine; } set { debugLine = value; } } - public DebugCode Code - { get { return status; } set { status = value; } } - } -} diff --git a/OpenSubtitlesHandler/Interfaces/IMethodResponse.cs b/OpenSubtitlesHandler/Interfaces/IMethodResponse.cs deleted file mode 100644 index 3450beff59..0000000000 --- a/OpenSubtitlesHandler/Interfaces/IMethodResponse.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; - -namespace OpenSubtitlesHandler -{ - /// - /// When you call a method to communicate with OpenSubtitles server, that method should return this response with the reuired information. - /// - public abstract class IMethodResponse - { - public IMethodResponse() { LoadAttributes(); } - public IMethodResponse(string name, string message) - { - this.name = name; - this.message = message; - } - protected string name; - protected string message; - protected double seconds; - protected string status; - - protected void LoadAttributes() - { - //foreach (Attribute attr in Attribute.GetCustomAttributes(this.GetType())) - //{ - // if (attr.GetType() == typeof(MethodResponseDescription)) - // { - // this.name = ((MethodResponseDescription)attr).Name; - // this.message = ((MethodResponseDescription)attr).Message; - // break; - // } - //} - } - - [Description("The name of this response"), Category("MethodResponse")] - public virtual string Name { get { return name; } set { name = value; } } - [Description("The message about this response"), Category("MethodResponse")] - public virtual string Message { get { return message; } set { message = value; } } - [Description("Time taken to execute this command on server"), Category("MethodResponse")] - public double Seconds { get { return seconds; } set { seconds = value; } } - [Description("The status"), Category("MethodResponse")] - public string Status { get { return status; } set { status = value; } } - } - - public class DescriptionAttribute : Attribute - { - public DescriptionAttribute(string text) { } - } - - public class CategoryAttribute : Attribute - { - public CategoryAttribute(string text) { } - } -} diff --git a/OpenSubtitlesHandler/Interfaces/MethodResponseAttr.cs b/OpenSubtitlesHandler/Interfaces/MethodResponseAttr.cs deleted file mode 100644 index a7e49032de..0000000000 --- a/OpenSubtitlesHandler/Interfaces/MethodResponseAttr.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; - -namespace OpenSubtitlesHandler -{ - /// - /// Attributes to describe a method response object. - /// - public class MethodResponseDescription : Attribute - { - public MethodResponseDescription(string name, string message) - { - this.name = name; - this.message = message; - } - - private string name; - private string message; - - public string Name { get { return name; } set { name = value; } } - public string Message { get { return message; } set { message = value; } } - } -} diff --git a/OpenSubtitlesHandler/Languages/DetectLanguageResult.cs b/OpenSubtitlesHandler/Languages/DetectLanguageResult.cs deleted file mode 100644 index 3e72dc65c4..0000000000 --- a/OpenSubtitlesHandler/Languages/DetectLanguageResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - public struct DetectLanguageResult - { - private string inputSample; - private string languageID; - - public string LanguageID - { get { return languageID; } set { languageID = value; } } - public string InputSample - { get { return inputSample; } set { inputSample = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseAddComment.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseAddComment.cs deleted file mode 100644 index 03d0337d4c..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseAddComment.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("AddComment method response", - "AddComment method response hold all expected values from server.")] - public class MethodResponseAddComment : IMethodResponse - { - public MethodResponseAddComment() - : base() - { } - public MethodResponseAddComment(string name, string message) - : base(name, message) - { } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseAddRequest.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseAddRequest.cs deleted file mode 100644 index b996043c28..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseAddRequest.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("AddRequest method response", - "AddRequest method response hold all expected values from server.")] - public class MethodResponseAddRequest : IMethodResponse - { - public MethodResponseAddRequest() - : base() - { } - public MethodResponseAddRequest(string name, string message) - : base(name, message) - { } - private string _request_url; - - public string request_url { get { return _request_url; } set { _request_url = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseAutoUpdate.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseAutoUpdate.cs deleted file mode 100644 index 6ee14f1f8f..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseAutoUpdate.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("AutoUpdate method response", - "AutoUpdate method response hold all expected values from server.")] - public class MethodResponseAutoUpdate : IMethodResponse - { - public MethodResponseAutoUpdate() - : base() - { } - public MethodResponseAutoUpdate(string name, string message) - : base(name, message) - { } - - private string _version; - private string _url_windows; - private string _comments; - private string _url_linux; - /// - /// Latest application version - /// - [Description("Latest application version"), Category("AutoUpdate")] - public string version { get { return _version; } set { _version = value; } } - /// - /// Download URL for Windows version - /// - [Description("Download URL for Windows version"), Category("AutoUpdate")] - public string url_windows { get { return _url_windows; } set { _url_windows = value; } } - /// - /// Application changelog and other comments - /// - [Description("Application changelog and other comments"), Category("AutoUpdate")] - public string comments { get { return _comments; } set { _comments = value; } } - /// - /// Download URL for Linux version - /// - [Description("Download URL for Linux version"), Category("AutoUpdate")] - public string url_linux { get { return _url_linux; } set { _url_linux = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash.cs deleted file mode 100644 index 4bb3262442..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("CheckMovieHash method response", - "CheckMovieHash method response hold all expected values from server.")] - public class MethodResponseCheckMovieHash : IMethodResponse - { - public MethodResponseCheckMovieHash() - : base() - { } - public MethodResponseCheckMovieHash(string name, string message) - : base(name, message) - { } - private List results = new List(); - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash2.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash2.cs deleted file mode 100644 index 72b4c3b152..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckMovieHash2.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("CheckMovieHash2 method response", - "CheckMovieHash2 method response hold all expected values from server.")] - public class MethodResponseCheckMovieHash2 : IMethodResponse - { - public MethodResponseCheckMovieHash2() - : base() - { } - public MethodResponseCheckMovieHash2(string name, string message) - : base(name, message) - { } - private List results = new List(); - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckSubHash.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckSubHash.cs deleted file mode 100644 index 04e287ea74..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseCheckSubHash.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("CheckSubHash method response", - "CheckSubHash method response hold all expected values from server.")] - public class MethodResponseCheckSubHash : IMethodResponse - { - public MethodResponseCheckSubHash() - : base() - { } - public MethodResponseCheckSubHash(string name, string message) - : base(name, message) - { } - private List results = new List(); - - public List Results { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseDetectLanguage.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseDetectLanguage.cs deleted file mode 100644 index 6bf21d50e8..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseDetectLanguage.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("DetectLanguage method response", - "DetectLanguage method response hold all expected values from server.")] - public class MethodResponseDetectLanguage : IMethodResponse - { - public MethodResponseDetectLanguage() - : base() - { } - public MethodResponseDetectLanguage(string name, string message) - : base(name, message) - { } - private List results = new List(); - - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseError.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseError.cs deleted file mode 100644 index 7ed807067e..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseError.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - /// - /// Response that can be used for general error like internet connection fail. - /// - [MethodResponseDescription("Error method response", - "Error method response that describes error that occured")] - public class MethodResponseError : IMethodResponse - { - public MethodResponseError() - : base() - { } - public MethodResponseError(string name, string message) - : base(name, message) - { } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetAvailableTranslations.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseGetAvailableTranslations.cs deleted file mode 100644 index 91803f4b3d..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetAvailableTranslations.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("GetAvailableTranslations method response", - "GetAvailableTranslations method response hold all expected values from server.")] - public class MethodResponseGetAvailableTranslations : IMethodResponse - { - public MethodResponseGetAvailableTranslations() - : base() - { } - public MethodResponseGetAvailableTranslations(string name, string message) - : base(name, message) - { } - private List results = new List(); - public List Results { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetComments.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseGetComments.cs deleted file mode 100644 index 421e1783bc..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetComments.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("GetComments method response", - "GetComments method response hold all expected values from server.")] - public class MethodResponseGetComments : IMethodResponse - { - public MethodResponseGetComments() - : base() - { } - public MethodResponseGetComments(string name, string message) - : base(name, message) - { } - private List results = new List(); - - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetSubLanguages.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseGetSubLanguages.cs deleted file mode 100644 index 905b87c916..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetSubLanguages.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("GetSubLanguages method response", - "GetSubLanguages method response hold all expected values from server.")] - public class MethodResponseGetSubLanguages : IMethodResponse - { - public MethodResponseGetSubLanguages() - : base() - { } - public MethodResponseGetSubLanguages(string name, string message) - : base(name, message) - { } - private List languages = new List(); - - public List Languages - { get { return languages; } set { languages = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetTranslation.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseGetTranslation.cs deleted file mode 100644 index f008f7711d..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseGetTranslation.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("GetTranslation method response", - "GetTranslation method response hold all expected values from server.")] - public class MethodResponseGetTranslation : IMethodResponse - { - public MethodResponseGetTranslation() - : base() - { } - public MethodResponseGetTranslation(string name, string message) - : base(name, message) - { } - private string data; - - public string ContentData { get { return data; } set { data = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovie.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovie.cs deleted file mode 100644 index 1148b5f479..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovie.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("InsertMovie method response", - "InsertMovie method response hold all expected values from server.")] - public class MethodResponseInsertMovie : IMethodResponse - { - public MethodResponseInsertMovie() - : base() - { } - public MethodResponseInsertMovie(string name, string message) - : base(name, message) - { } - private string _ID; - public string ID - { get { return _ID; } set { _ID = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovieHash.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovieHash.cs deleted file mode 100644 index 74f52837c9..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseInsertMovieHash.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("InsertMovieHash method response", - "InsertMovieHash method response hold all expected values from server.")] - public class MethodResponseInsertMovieHash : IMethodResponse - { - public MethodResponseInsertMovieHash() - : base() - { } - public MethodResponseInsertMovieHash(string name, string message) - : base(name, message) - { } - private List _accepted_moviehashes = new List(); - private List _new_imdbs = new List(); - - public List accepted_moviehashes { get { return _accepted_moviehashes; } set { _accepted_moviehashes = value; } } - public List new_imdbs { get { return _new_imdbs; } set { _new_imdbs = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseLogIn.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseLogIn.cs deleted file mode 100644 index 44d2943824..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseLogIn.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - /// - /// Response can be used for log in/out operation. - /// - [MethodResponseDescription("LogIn method response", - "LogIn method response hold all expected values from server.")] - public class MethodResponseLogIn : IMethodResponse - { - public MethodResponseLogIn() - : base() - { } - public MethodResponseLogIn(string name, string message) - : base(name, message) - { } - private string token; - - public string Token { get { return token; } set { token = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieDetails.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieDetails.cs deleted file mode 100644 index 6126c0053a..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieDetails.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("MovieDetails method response", - "MovieDetails method response hold all expected values from server.")] - public class MethodResponseMovieDetails : IMethodResponse - { - public MethodResponseMovieDetails() - : base() - { } - public MethodResponseMovieDetails(string name, string message) - : base(name, message) - { } - // Details - private string id; - private string title; - private string year; - private string coverLink; - - private string duration; - private string tagline; - private string plot; - private string goofs; - private string trivia; - private List cast = new List(); - private List directors = new List(); - private List writers = new List(); - private List awards = new List(); - private List genres = new List(); - private List country = new List(); - private List language = new List(); - private List certification = new List(); - - // Details - public string ID { get { return id; } set { id = value; } } - public string Title { get { return title; } set { title = value; } } - public string Year { get { return year; } set { year = value; } } - public string CoverLink { get { return coverLink; } set { coverLink = value; } } - public string Duration { get { return duration; } set { duration = value; } } - public string Tagline { get { return tagline; } set { tagline = value; } } - public string Plot { get { return plot; } set { plot = value; } } - public string Goofs { get { return goofs; } set { goofs = value; } } - public string Trivia { get { return trivia; } set { trivia = value; } } - public List Cast { get { return cast; } set { cast = value; } } - public List Directors { get { return directors; } set { directors = value; } } - public List Writers { get { return writers; } set { writers = value; } } - public List Genres { get { return genres; } set { genres = value; } } - public List Awards { get { return awards; } set { awards = value; } } - public List Country { get { return country; } set { country = value; } } - public List Language { get { return language; } set { language = value; } } - public List Certification { get { return certification; } set { certification = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieSearch.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieSearch.cs deleted file mode 100644 index 93cd703462..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseMovieSearch.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("MovieSearch method response", - "MovieSearch method response hold all expected values from server.")] - public class MethodResponseMovieSearch : IMethodResponse - { - public MethodResponseMovieSearch() - : base() - { - results = new List(); - } - public MethodResponseMovieSearch(string name, string message) - : base(name, message) - { - results = new List(); - } - private List results; - - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseNoOperation.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseNoOperation.cs deleted file mode 100644 index 02a9993cb1..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseNoOperation.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("NoOperation method response", - "NoOperation method response hold all expected values from server.")] - public class MethodResponseNoOperation : IMethodResponse - { - public MethodResponseNoOperation() - : base() { } - public MethodResponseNoOperation(string name, string message) - : base(name, message) - { } - - private string _global_wrh_download_limit; - private string _client_ip; - private string _limit_check_by; - private string _client_24h_download_count; - private string _client_downlaod_quota; - private string _client_24h_download_limit; - - public string global_wrh_download_limit - { get { return _global_wrh_download_limit; } set { _global_wrh_download_limit = value; } } - public string client_ip - { get { return _client_ip; } set { _client_ip = value; } } - public string limit_check_by - { get { return _limit_check_by; } set { _limit_check_by = value; } } - public string client_24h_download_count - { get { return _client_24h_download_count; } set { _client_24h_download_count = value; } } - public string client_downlaod_quota - { get { return _client_downlaod_quota; } set { _client_downlaod_quota = value; } } - public string client_24h_download_limit - { get { return _client_24h_download_limit; } set { _client_24h_download_limit = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongImdbMovie.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongImdbMovie.cs deleted file mode 100644 index 391fec58a0..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongImdbMovie.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("ReportWrongImdbMovie method response", - "ReportWrongImdbMovie method response hold all expected values from server.")] - public class MethodResponseReportWrongImdbMovie : IMethodResponse - { - public MethodResponseReportWrongImdbMovie() - : base() - { } - public MethodResponseReportWrongImdbMovie(string name, string message) - : base(name, message) - { } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongMovieHash.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongMovieHash.cs deleted file mode 100644 index 5696e7084c..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseReportWrongMovieHash.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("ReportWrongMovieHash method response", - "ReportWrongMovieHash method response hold all expected values from server.")] - public class MethodResponseReportWrongMovieHash : IMethodResponse - { - public MethodResponseReportWrongMovieHash() - : base() - { } - public MethodResponseReportWrongMovieHash(string name, string message) - : base(name, message) - { } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseSearchToMail.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseSearchToMail.cs deleted file mode 100644 index ea248bc22c..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseSearchToMail.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("SearchToMail method response", - "SearchToMail method response hold all expected values from server.")] - public class MethodResponseSearchToMail : IMethodResponse - { - public MethodResponseSearchToMail() - : base() - { } - public MethodResponseSearchToMail(string name, string message) - : base(name, message) - { } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseServerInfo.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseServerInfo.cs deleted file mode 100644 index 973550e9f0..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseServerInfo.cs +++ /dev/null @@ -1,131 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("ServerInfo method response", - "ServerInfo method response hold all expected values from server.")] - public class MethodResponseServerInfo : IMethodResponse - { - public MethodResponseServerInfo() - : base() - { } - public MethodResponseServerInfo(string name, string message) - : base(name, message) - { } - private string _xmlrpc_version; - private string _xmlrpc_url; - private string _application; - private string _contact; - private string _website_url; - private int _users_online_total; - private int _users_online_program; - private int _users_loggedin; - private string _users_max_alltime; - private string _users_registered; - private string _subs_downloads; - private string _subs_subtitle_files; - private string _movies_total; - private string _movies_aka; - private string _total_subtitles_languages; - private List _last_update_strings = new List(); - - /// - /// Version of server's XML-RPC API implementation - /// - [Description("Version of server's XML-RPC API implementation"), Category("OS")] - public string xmlrpc_version { get { return _xmlrpc_version; } set { _xmlrpc_version = value; } } - /// - /// XML-RPC interface URL - /// - [Description("XML-RPC interface URL"), Category("OS")] - public string xmlrpc_url { get { return _xmlrpc_url; } set { _xmlrpc_url = value; } } - /// - /// Server's application name and version - /// - [Description("Server's application name and version"), Category("OS")] - public string application { get { return _application; } set { _application = value; } } - /// - /// Contact e-mail address for server related quuestions and problems - /// - [Description("Contact e-mail address for server related quuestions and problems"), Category("OS")] - public string contact { get { return _contact; } set { _contact = value; } } - /// - /// Main server URL - /// - [Description("Main server URL"), Category("OS")] - public string website_url { get { return _website_url; } set { _website_url = value; } } - /// - /// Number of users currently online - /// - [Description("Number of users currently online"), Category("OS")] - public int users_online_total { get { return _users_online_total; } set { _users_online_total = value; } } - /// - /// Number of users currently online using a client application (XML-RPC API) - /// - [Description("Number of users currently online using a client application (XML-RPC API)"), Category("OS")] - public int users_online_program { get { return _users_online_program; } set { _users_online_program = value; } } - /// - /// Number of currently logged-in users - /// - [Description("Number of currently logged-in users"), Category("OS")] - public int users_loggedin { get { return _users_loggedin; } set { _users_loggedin = value; } } - /// - /// Maximum number of users throughout the history - /// - [Description("Maximum number of users throughout the history"), Category("OS")] - public string users_max_alltime { get { return _users_max_alltime; } set { _users_max_alltime = value; } } - /// - /// Number of registered users - /// - [Description("Number of registered users"), Category("OS")] - public string users_registered { get { return _users_registered; } set { _users_registered = value; } } - /// - /// Total number of subtitle downloads - /// - [Description("Total number of subtitle downloads"), Category("OS")] - public string subs_downloads { get { return _subs_downloads; } set { _subs_downloads = value; } } - /// - /// Total number of subtitle files stored on the server - /// - [Description("Total number of subtitle files stored on the server"), Category("OS")] - public string subs_subtitle_files { get { return _subs_subtitle_files; } set { _subs_subtitle_files = value; } } - /// - /// Total number of movies in the database - /// - [Description("Total number of movies in the database"), Category("OS")] - public string movies_total { get { return _movies_total; } set { _movies_total = value; } } - /// - /// Total number of movie A.K.A. titles in the database - /// - [Description("Total number of movie A.K.A. titles in the database"), Category("OS")] - public string movies_aka { get { return _movies_aka; } set { _movies_aka = value; } } - /// - /// Total number of subtitle languages supported - /// - [Description("Total number of subtitle languages supported"), Category("OS")] - public string total_subtitles_languages { get { return _total_subtitles_languages; } set { _total_subtitles_languages = value; } } - /// - /// Structure containing information about last updates of translations. - /// - [Description("Structure containing information about last updates of translations"), Category("OS")] - public List last_update_strings { get { return _last_update_strings; } set { _last_update_strings = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleDownload.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleDownload.cs deleted file mode 100644 index 6a5d57d19a..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleDownload.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; -namespace OpenSubtitlesHandler -{ - /// - /// The response that should be used by subtitle download methods. - /// - [MethodResponseDescription("SubtitleDownload method response", - "SubtitleDownload method response hold all expected values from server.")] - public class MethodResponseSubtitleDownload : IMethodResponse - { - public MethodResponseSubtitleDownload() - : base() - { - results = new List(); - } - public MethodResponseSubtitleDownload(string name, string message) - : base(name, message) - { - results = new List(); - } - private List results; - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleSearch.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleSearch.cs deleted file mode 100644 index 0dce203495..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitleSearch.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - /// - /// Response to SearchSubtitle successed call. - /// - [MethodResponseDescription("SubtitleSearch method response", - "SubtitleSearch method response hold all expected values from server.")] - public class MethodResponseSubtitleSearch : IMethodResponse - { - public MethodResponseSubtitleSearch() - : base() - { - results = new List(); - } - public MethodResponseSubtitleSearch(string name, string message) - : base(name, message) - { - results = new List(); - } - - private List results; - public List Results - { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitlesVote.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitlesVote.cs deleted file mode 100644 index f02f822f05..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseSubtitlesVote.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("SubtitlesVote method response", - "SubtitlesVote method response hold all expected values from server.")] - public class MethodResponseSubtitlesVote : IMethodResponse - { - public MethodResponseSubtitlesVote() - : base() - { } - public MethodResponseSubtitlesVote(string name, string message) - : base(name, message) - { } - private string _SubRating; - private string _SubSumVotes; - private string _IDSubtitle; - - public string SubRating - { get { return _SubRating; } set { _SubRating = value; } } - public string SubSumVotes - { get { return _SubSumVotes; } set { _SubSumVotes = value; } } - public string IDSubtitle - { get { return _IDSubtitle; } set { _IDSubtitle = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseTryUploadSubtitles.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseTryUploadSubtitles.cs deleted file mode 100644 index cb3866a628..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseTryUploadSubtitles.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("TryUploadSubtitles method response", - "TryUploadSubtitles method response hold all expected values from server.")] - public class MethodResponseTryUploadSubtitles : IMethodResponse - { - public MethodResponseTryUploadSubtitles() - : base() - { } - public MethodResponseTryUploadSubtitles(string name, string message) - : base(name, message) - { } - private int alreadyindb; - private List results = new List(); - - public int AlreadyInDB { get { return alreadyindb; } set { alreadyindb = value; } } - public List Results { get { return results; } set { results = value; } } - } -} diff --git a/OpenSubtitlesHandler/MethodResponses/MethodResponseUploadSubtitles.cs b/OpenSubtitlesHandler/MethodResponses/MethodResponseUploadSubtitles.cs deleted file mode 100644 index bda950befc..0000000000 --- a/OpenSubtitlesHandler/MethodResponses/MethodResponseUploadSubtitles.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - [MethodResponseDescription("UploadSubtitles method response", - "UploadSubtitles method response hold all expected values from server.")] - public class MethodResponseUploadSubtitles : IMethodResponse - { - public MethodResponseUploadSubtitles() - : base() - { } - public MethodResponseUploadSubtitles(string name, string message) - : base(name, message) - { } - private string _data; - private bool _subtitles; - - public string Data { get { return _data; } set { _data = value; } } - public bool SubTitles { get { return _subtitles; } set { _subtitles = value; } } - } -} diff --git a/OpenSubtitlesHandler/Methods Implemeted.txt b/OpenSubtitlesHandler/Methods Implemeted.txt deleted file mode 100644 index e3493d9a2c..0000000000 --- a/OpenSubtitlesHandler/Methods Implemeted.txt +++ /dev/null @@ -1,39 +0,0 @@ -List of available OpenSubtitles.org server XML-RPC methods. -========================================================== -Legends: -* OK: this method is fully implemented, tested and works fine. -* TODO: this method is in the plan to be added. -* NOT TESTED: this method added and expected to work fine but never tested. -* NOT WORK (x): this method added but not work. x= Description of the error. - --------------------------------------------- -Method name | Status --------------------------|------------------ -LogIn | OK -LogOut | OK -NoOperation | OK -SearchSubtitles | OK -DownloadSubtitles | OK -SearchToMail | OK -TryUploadSubtitles | OK -UploadSubtitles | OK -SearchMoviesOnIMDB | OK -GetIMDBMovieDetails | OK -InsertMovie | OK -InsertMovieHash | OK -ServerInfo | OK -ReportWrongMovieHash | OK -ReportWrongImdbMovie | OK -SubtitlesVote | OK -AddComment | OK -AddRequest | OK -GetComments | OK -GetSubLanguages | OK -DetectLanguage | OK -GetAvailableTranslations | OK -GetTranslation | NOT WORK (Returns status of error 410 'Other or unknown error') -AutoUpdate | NOT WORK (Returns status: 'parse error. not well formed') -CheckMovieHash | OK -CheckMovieHash2 | OK -CheckSubHash | OK --------------------------------------------- diff --git a/OpenSubtitlesHandler/MovieHasher.cs b/OpenSubtitlesHandler/MovieHasher.cs deleted file mode 100644 index 25d91c1aca..0000000000 --- a/OpenSubtitlesHandler/MovieHasher.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace OpenSubtitlesHandler -{ - public class MovieHasher - { - public static byte[] ComputeMovieHash(Stream input) - { - using (input) - { - long lhash, streamsize; - streamsize = input.Length; - lhash = streamsize; - - long i = 0; - byte[] buffer = new byte[sizeof(long)]; - while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) - { - i++; - lhash += BitConverter.ToInt64(buffer, 0); - } - - input.Position = Math.Max(0, streamsize - 65536); - i = 0; - while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) - { - i++; - lhash += BitConverter.ToInt64(buffer, 0); - } - byte[] result = BitConverter.GetBytes(lhash); - Array.Reverse(result); - return result; - } - } - - public static string ToHexadecimal(byte[] bytes) - { - var hexBuilder = new StringBuilder(); - for (int i = 0; i < bytes.Length; i++) - { - hexBuilder.Append(bytes[i].ToString("x2")); - } - return hexBuilder.ToString(); - } - } -} diff --git a/OpenSubtitlesHandler/Movies/CheckMovieHash2Data.cs b/OpenSubtitlesHandler/Movies/CheckMovieHash2Data.cs deleted file mode 100644 index 9535901375..0000000000 --- a/OpenSubtitlesHandler/Movies/CheckMovieHash2Data.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct CheckMovieHash2Data - { - private string _MovieHash; - private string _MovieImdbID; - private string _MovieName; - private string _MovieYear; - private string _MovieKind; - private string _SeriesSeason; - private string _SeriesEpisode; - private string _SeenCount; - - public string MovieHash { get { return _MovieHash; } set { _MovieHash = value; } } - public string MovieImdbID { get { return _MovieImdbID; } set { _MovieImdbID = value; } } - public string MovieName { get { return _MovieName; } set { _MovieName = value; } } - public string MovieYear { get { return _MovieYear; } set { _MovieYear = value; } } - public string MovieKind { get { return _MovieKind; } set { _MovieKind = value; } } - public string SeriesSeason { get { return _SeriesSeason; } set { _SeriesSeason = value; } } - public string SeriesEpisode { get { return _SeriesEpisode; } set { _SeriesEpisode = value; } } - public string SeenCount { get { return _SeenCount; } set { _SeenCount = value; } } - } -} diff --git a/OpenSubtitlesHandler/Movies/CheckMovieHash2Result.cs b/OpenSubtitlesHandler/Movies/CheckMovieHash2Result.cs deleted file mode 100644 index 96652fae74..0000000000 --- a/OpenSubtitlesHandler/Movies/CheckMovieHash2Result.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - public class CheckMovieHash2Result - { - private string name; - private List data = new List(); - - public string Name { get { return name; } set { name = value; } } - public List Items { get { return data; } set { data = value; } } - - public override string ToString() - { - return name; - } - } -} diff --git a/OpenSubtitlesHandler/Movies/CheckMovieHashResult.cs b/OpenSubtitlesHandler/Movies/CheckMovieHashResult.cs deleted file mode 100644 index 0d6c79f80b..0000000000 --- a/OpenSubtitlesHandler/Movies/CheckMovieHashResult.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct CheckMovieHashResult - { - private string name; - private string _MovieHash; - private string _MovieImdbID; - private string _MovieName; - private string _MovieYear; - - public string MovieHash { get { return _MovieHash; } set { _MovieHash = value; } } - public string MovieImdbID { get { return _MovieImdbID; } set { _MovieImdbID = value; } } - public string MovieName { get { return _MovieName; } set { _MovieName = value; } } - public string MovieYear { get { return _MovieYear; } set { _MovieYear = value; } } - public string Name { get { return name; } set { name = value; } } - - public override string ToString() - { - return name; - } - } -} diff --git a/OpenSubtitlesHandler/Movies/InsertMovieHashParameters.cs b/OpenSubtitlesHandler/Movies/InsertMovieHashParameters.cs deleted file mode 100644 index d0de7f8c64..0000000000 --- a/OpenSubtitlesHandler/Movies/InsertMovieHashParameters.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public class InsertMovieHashParameters - { - private string _moviehash = ""; - private string _moviebytesize = ""; - private string _imdbid = ""; - private string _movietimems = ""; - private string _moviefps = ""; - private string _moviefilename = ""; - - public string moviehash { get { return _moviehash; } set { _moviehash = value; } } - public string moviebytesize { get { return _moviebytesize; } set { _moviebytesize = value; } } - public string imdbid { get { return _imdbid; } set { _imdbid = value; } } - public string movietimems { get { return _movietimems; } set { _movietimems = value; } } - public string moviefps { get { return _moviefps; } set { _moviefps = value; } } - public string moviefilename { get { return _moviefilename; } set { _moviefilename = value; } } - } -} diff --git a/OpenSubtitlesHandler/Movies/MovieSearchResult.cs b/OpenSubtitlesHandler/Movies/MovieSearchResult.cs deleted file mode 100644 index d771165830..0000000000 --- a/OpenSubtitlesHandler/Movies/MovieSearchResult.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct MovieSearchResult - { - private string id; - private string title; - - public string ID - { get { return id; } set { id = value; } } - public string Title - { get { return title; } set { title = value; } } - /// - /// Title - /// - /// - public override string ToString() - { - return title; - } - } -} diff --git a/OpenSubtitlesHandler/Movies/SearchToMailMovieParameter.cs b/OpenSubtitlesHandler/Movies/SearchToMailMovieParameter.cs deleted file mode 100644 index a0ecc87f8a..0000000000 --- a/OpenSubtitlesHandler/Movies/SearchToMailMovieParameter.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct SearchToMailMovieParameter - { - private string _moviehash; - private double _moviesize; - - public string moviehash { get { return _moviehash; } set { _moviehash = value; } } - public double moviesize { get { return _moviesize; } set { _moviesize = value; } } - } -} diff --git a/OpenSubtitlesHandler/OpenSubtitles.cs b/OpenSubtitlesHandler/OpenSubtitles.cs deleted file mode 100644 index 76f70dc070..0000000000 --- a/OpenSubtitlesHandler/OpenSubtitles.cs +++ /dev/null @@ -1,2744 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using OpenSubtitlesHandler.Console; -using XmlRpcHandler; - -namespace OpenSubtitlesHandler -{ - /// - /// The core of the OpenSubtitles Handler library. All members are static. - /// - public sealed class OpenSubtitles - { - private static string XML_PRC_USERAGENT = ""; - // This is session id after log in, important value and MUST be set by LogIn before any other call. - private static string TOKEN = ""; - - /// - /// Set the useragent value. This must be called before doing anything else. - /// - /// The useragent value - public static void SetUserAgent(string agent) - { - XML_PRC_USERAGENT = agent; - } - - /*Session handling*/ - /// - /// Send a LogIn request, this must be called before anything else in this class. - /// - /// The user name of OpenSubtitles - /// The password - /// The language, usally en - /// Status of the login operation - public static IMethodResponse LogIn(string userName, string password, string language) - { - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(userName)); - parms.Add(new XmlRpcValueBasic(password)); - parms.Add(new XmlRpcValueBasic(language)); - parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT)); - var call = new XmlRpcMethodCall("LogIn", parms); - OSHConsole.WriteLine("Sending LogIn request to the server ...", DebugCode.Good); - - //File.WriteAllText(".\\request.txt", Encoding.UTF8.GetString(XmlRpcGenerator.Generate(call))); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. We expect Struct here. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - var re = new MethodResponseLogIn("Success", "Log in successful."); - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "token": re.Token = TOKEN = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": re.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "status": re.Status = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - return re; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "Log in failed !"); - } - - public static async Task LogInAsync(string userName, string password, string language, CancellationToken cancellationToken) - { - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(userName)); - parms.Add(new XmlRpcValueBasic(password)); - parms.Add(new XmlRpcValueBasic(language)); - parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT)); - var call = new XmlRpcMethodCall("LogIn", parms); - OSHConsole.WriteLine("Sending LogIn request to the server ...", DebugCode.Good); - - //File.WriteAllText(".\\request.txt", Encoding.UTF8.GetString(XmlRpcGenerator.Generate(call))); - // Send the request to the server - var stream = await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken) - .ConfigureAwait(false); - - string response = Utilities.GetStreamString(stream); - - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. We expect Struct here. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - var re = new MethodResponseLogIn("Success", "Log in successful."); - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "token": - re.Token = TOKEN = MEMBER.Data.Data.ToString(); - OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "seconds": - re.Seconds = double.Parse(MEMBER.Data.Data.ToString(), CultureInfo.InvariantCulture); - OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "status": - re.Status = MEMBER.Data.Data.ToString(); - OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - } - } - return re; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "Log in failed !"); - } - - /// - /// Log out from the server. Call this to terminate the session. - /// - /// - public static IMethodResponse LogOut() - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var call = new XmlRpcMethodCall("LogOut", parms); - - OSHConsole.WriteLine("Sending LogOut request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. We expect Struct here. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var strct = (XmlRpcValueStruct)calls[0].Parameters[0]; - OSHConsole.WriteLine("STATUS=" + ((XmlRpcValueBasic)strct.Members[0].Data).Data.ToString()); - OSHConsole.WriteLine("SECONDS=" + ((XmlRpcValueBasic)strct.Members[1].Data).Data.ToString()); - var re = new MethodResponseLogIn("Success", "Log out successful."); - re.Status = ((XmlRpcValueBasic)strct.Members[0].Data).Data.ToString(); - re.Seconds = (double)((XmlRpcValueBasic)strct.Members[1].Data).Data; - return re; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "Log out failed !"); - } - /// - /// keep-alive user's session, verify token/session validity - /// - /// Status of the call operation - public static IMethodResponse NoOperation() - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT, XmlRpcBasicValueType.String)); - var call = new XmlRpcMethodCall("NoOperation", parms); - - OSHConsole.WriteLine("Sending NoOperation request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. We expect Struct here. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var R = new MethodResponseNoOperation(); - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data); break; - case "download_limits": - var dlStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dlmember in dlStruct.Members) - { - OSHConsole.WriteLine(" >" + dlmember.Name + "= " + dlmember.Data.Data.ToString()); - switch (dlmember.Name) - { - case "global_wrh_download_limit": R.global_wrh_download_limit = dlmember.Data.Data.ToString(); break; - case "client_ip": R.client_ip = dlmember.Data.Data.ToString(); break; - case "limit_check_by": R.limit_check_by = dlmember.Data.Data.ToString(); break; - case "client_24h_download_count": R.client_24h_download_count = dlmember.Data.Data.ToString(); break; - case "client_downlaod_quota": R.client_downlaod_quota = dlmember.Data.Data.ToString(); break; - case "client_24h_download_limit": R.client_24h_download_limit = dlmember.Data.Data.ToString(); break; - } - } - break; - } - } - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "NoOperation call failed !"); - } - /*Search and download*/ - /// - /// Search for subtitle files matching your videos using either video file hashes or IMDb IDs. - /// - /// List of search subtitle parameters which each one represents 'struct parameter' as descriped at http://trac.opensubtitles.org/projects/opensubtitles/wiki/XmlRpcSearchSubtitles - /// Status of the call operation. If the call success the response will be 'MethodResponseSubtitleSearch' - public static IMethodResponse SearchSubtitles(SubtitleSearchParameters[] parameters) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - if (parameters == null) - { - OSHConsole.UpdateLine("No subtitle search parameter passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle search parameter passed"); - } - if (parameters.Length == 0) - { - OSHConsole.UpdateLine("No subtitle search parameter passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle search parameter passed"); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add subtitle search parameters. Each one will be like 'array' of structs. - var array = new XmlRpcValueArray(); - foreach (SubtitleSearchParameters param in parameters) - { - var strct = new XmlRpcValueStruct(new List()); - // sublanguageid member - var member = new XmlRpcStructMember("sublanguageid", - new XmlRpcValueBasic(param.SubLangaugeID, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - // moviehash member - if (param.MovieHash.Length > 0 && param.MovieByteSize > 0) - { - member = new XmlRpcStructMember("moviehash", - new XmlRpcValueBasic(param.MovieHash, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - // moviehash member - member = new XmlRpcStructMember("moviebytesize", - new XmlRpcValueBasic(param.MovieByteSize, XmlRpcBasicValueType.Int)); - strct.Members.Add(member); - } - if (param.Query.Length > 0) - { - member = new XmlRpcStructMember("query", - new XmlRpcValueBasic(param.Query, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - - if (param.Episode.Length > 0 && param.Season.Length > 0) - { - member = new XmlRpcStructMember("season", - new XmlRpcValueBasic(param.Season, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - member = new XmlRpcStructMember("episode", - new XmlRpcValueBasic(param.Episode, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - - // imdbid member - if (param.IMDbID.Length > 0) - { - member = new XmlRpcStructMember("imdbid", - new XmlRpcValueBasic(param.IMDbID, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - // Add the struct to the array - array.Values.Add(strct); - } - // Add the array to the parameters - parms.Add(array); - // Call ! - var call = new XmlRpcMethodCall("SearchSubtitles", parms); - OSHConsole.WriteLine("Sending SearchSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - // We expect Struct of 3 members: - //* the first is status - //* the second is [array of structs, each one includes subtitle file]. - //* the third is [double basic value] represent seconds token by server. - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSubtitleSearch(); - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Search results: "); - - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new SubtitleSearchResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "IDMovie": result.IDMovie = submember.Data.Data.ToString(); break; - case "IDMovieImdb": result.IDMovieImdb = submember.Data.Data.ToString(); break; - case "IDSubMovieFile": result.IDSubMovieFile = submember.Data.Data.ToString(); break; - case "IDSubtitle": result.IDSubtitle = submember.Data.Data.ToString(); break; - case "IDSubtitleFile": result.IDSubtitleFile = submember.Data.Data.ToString(); break; - case "ISO639": result.ISO639 = submember.Data.Data.ToString(); break; - case "LanguageName": result.LanguageName = submember.Data.Data.ToString(); break; - case "MovieByteSize": result.MovieByteSize = submember.Data.Data.ToString(); break; - case "MovieHash": result.MovieHash = submember.Data.Data.ToString(); break; - case "MovieImdbRating": result.MovieImdbRating = submember.Data.Data.ToString(); break; - case "MovieName": result.MovieName = submember.Data.Data.ToString(); break; - case "MovieNameEng": result.MovieNameEng = submember.Data.Data.ToString(); break; - case "MovieReleaseName": result.MovieReleaseName = submember.Data.Data.ToString(); break; - case "MovieTimeMS": result.MovieTimeMS = submember.Data.Data.ToString(); break; - case "MovieYear": result.MovieYear = submember.Data.Data.ToString(); break; - case "SubActualCD": result.SubActualCD = submember.Data.Data.ToString(); break; - case "SubAddDate": result.SubAddDate = submember.Data.Data.ToString(); break; - case "SubAuthorComment": result.SubAuthorComment = submember.Data.Data.ToString(); break; - case "SubBad": result.SubBad = submember.Data.Data.ToString(); break; - case "SubDownloadLink": result.SubDownloadLink = submember.Data.Data.ToString(); break; - case "SubDownloadsCnt": result.SubDownloadsCnt = submember.Data.Data.ToString(); break; - case "SeriesEpisode": result.SeriesEpisode = submember.Data.Data.ToString(); break; - case "SeriesSeason": result.SeriesSeason = submember.Data.Data.ToString(); break; - case "SubFileName": result.SubFileName = submember.Data.Data.ToString(); break; - case "SubFormat": result.SubFormat = submember.Data.Data.ToString(); break; - case "SubHash": result.SubHash = submember.Data.Data.ToString(); break; - case "SubLanguageID": result.SubLanguageID = submember.Data.Data.ToString(); break; - case "SubRating": result.SubRating = submember.Data.Data.ToString(); break; - case "SubSize": result.SubSize = submember.Data.Data.ToString(); break; - case "SubSumCD": result.SubSumCD = submember.Data.Data.ToString(); break; - case "UserID": result.UserID = submember.Data.Data.ToString(); break; - case "UserNickName": result.UserNickName = submember.Data.Data.ToString(); break; - case "ZipDownloadLink": result.ZipDownloadLink = submember.Data.Data.ToString(); break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine(">" + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "Search Subtitles call failed !"); - } - - public static async Task SearchSubtitlesAsync(SubtitleSearchParameters[] parameters, CancellationToken cancellationToken) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - if (parameters == null) - { - OSHConsole.UpdateLine("No subtitle search parameter passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle search parameter passed"); - } - if (parameters.Length == 0) - { - OSHConsole.UpdateLine("No subtitle search parameter passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle search parameter passed"); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add subtitle search parameters. Each one will be like 'array' of structs. - var array = new XmlRpcValueArray(); - foreach (SubtitleSearchParameters param in parameters) - { - var strct = new XmlRpcValueStruct(new List()); - // sublanguageid member - var member = new XmlRpcStructMember("sublanguageid", - new XmlRpcValueBasic(param.SubLangaugeID, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - // moviehash member - if (param.MovieHash.Length > 0 && param.MovieByteSize > 0) - { - member = new XmlRpcStructMember("moviehash", - new XmlRpcValueBasic(param.MovieHash, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - // moviehash member - member = new XmlRpcStructMember("moviebytesize", - new XmlRpcValueBasic(param.MovieByteSize, XmlRpcBasicValueType.Int)); - strct.Members.Add(member); - } - if (param.Query.Length > 0) - { - member = new XmlRpcStructMember("query", - new XmlRpcValueBasic(param.Query, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - - if (param.Episode.Length > 0 && param.Season.Length > 0) - { - member = new XmlRpcStructMember("season", - new XmlRpcValueBasic(param.Season, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - member = new XmlRpcStructMember("episode", - new XmlRpcValueBasic(param.Episode, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - - // imdbid member - if (param.IMDbID.Length > 0) - { - member = new XmlRpcStructMember("imdbid", - new XmlRpcValueBasic(param.IMDbID, XmlRpcBasicValueType.String)); - strct.Members.Add(member); - } - // Add the struct to the array - array.Values.Add(strct); - } - // Add the array to the parameters - parms.Add(array); - // Call ! - var call = new XmlRpcMethodCall("SearchSubtitles", parms); - OSHConsole.WriteLine("Sending SearchSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken).ConfigureAwait(false)); - - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - // We expect Struct of 3 members: - //* the first is status - //* the second is [array of structs, each one includes subtitle file]. - //* the third is [double basic value] represent seconds token by server. - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSubtitleSearch(); - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Search results: "); - - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new SubtitleSearchResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "IDMovie": result.IDMovie = submember.Data.Data.ToString(); break; - case "IDMovieImdb": result.IDMovieImdb = submember.Data.Data.ToString(); break; - case "IDSubMovieFile": result.IDSubMovieFile = submember.Data.Data.ToString(); break; - case "IDSubtitle": result.IDSubtitle = submember.Data.Data.ToString(); break; - case "IDSubtitleFile": result.IDSubtitleFile = submember.Data.Data.ToString(); break; - case "ISO639": result.ISO639 = submember.Data.Data.ToString(); break; - case "LanguageName": result.LanguageName = submember.Data.Data.ToString(); break; - case "MovieByteSize": result.MovieByteSize = submember.Data.Data.ToString(); break; - case "MovieHash": result.MovieHash = submember.Data.Data.ToString(); break; - case "MovieImdbRating": result.MovieImdbRating = submember.Data.Data.ToString(); break; - case "MovieName": result.MovieName = submember.Data.Data.ToString(); break; - case "MovieNameEng": result.MovieNameEng = submember.Data.Data.ToString(); break; - case "MovieReleaseName": result.MovieReleaseName = submember.Data.Data.ToString(); break; - case "MovieTimeMS": result.MovieTimeMS = submember.Data.Data.ToString(); break; - case "MovieYear": result.MovieYear = submember.Data.Data.ToString(); break; - case "SubActualCD": result.SubActualCD = submember.Data.Data.ToString(); break; - case "SubAddDate": result.SubAddDate = submember.Data.Data.ToString(); break; - case "SubAuthorComment": result.SubAuthorComment = submember.Data.Data.ToString(); break; - case "SubBad": result.SubBad = submember.Data.Data.ToString(); break; - case "SubDownloadLink": result.SubDownloadLink = submember.Data.Data.ToString(); break; - case "SubDownloadsCnt": result.SubDownloadsCnt = submember.Data.Data.ToString(); break; - case "SeriesEpisode": result.SeriesEpisode = submember.Data.Data.ToString(); break; - case "SeriesSeason": result.SeriesSeason = submember.Data.Data.ToString(); break; - case "SubFileName": result.SubFileName = submember.Data.Data.ToString(); break; - case "SubFormat": result.SubFormat = submember.Data.Data.ToString(); break; - case "SubHash": result.SubHash = submember.Data.Data.ToString(); break; - case "SubLanguageID": result.SubLanguageID = submember.Data.Data.ToString(); break; - case "SubRating": result.SubRating = submember.Data.Data.ToString(); break; - case "SubSize": result.SubSize = submember.Data.Data.ToString(); break; - case "SubSumCD": result.SubSumCD = submember.Data.Data.ToString(); break; - case "UserID": result.UserID = submember.Data.Data.ToString(); break; - case "UserNickName": result.UserNickName = submember.Data.Data.ToString(); break; - case "ZipDownloadLink": result.ZipDownloadLink = submember.Data.Data.ToString(); break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine(">" + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "Search Subtitles call failed !"); - } - - /// - /// Download subtitle file(s) - /// - /// The subtitle IDS (an array of IDSubtitleFile value that given by server as SearchSubtiles results) - /// Status of the call operation. If the call success the response will be 'MethodResponseSubtitleDownload' which will hold downloaded subtitles - public static IMethodResponse DownloadSubtitles(int[] subIDS) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - if (subIDS == null) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - if (subIDS.Length == 0) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add subtitle search parameters. Each one will be like 'array' of structs. - var array = new XmlRpcValueArray(); - foreach (int id in subIDS) - { - array.Values.Add(new XmlRpcValueBasic(id, XmlRpcBasicValueType.Int)); - } - // Add the array to the parameters - parms.Add(array); - // Call ! - var call = new XmlRpcMethodCall("DownloadSubtitles", parms); - OSHConsole.WriteLine("Sending DownloadSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - // We expect Struct of 3 members: - //* the first is status - //* the second is [array of structs, each one includes subtitle file]. - //* the third is [double basic value] represent seconds token by server. - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSubtitleDownload(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Download results:"); - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new SubtitleDownloadResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "idsubtitlefile": result.IdSubtitleFile = (string)submember.Data.Data; break; - case "data": result.Data = (string)submember.Data.Data; break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine("> IDSubtilteFile= " + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "DownloadSubtitles call failed !"); - } - - public static async Task DownloadSubtitlesAsync(int[] subIDS, CancellationToken cancellationToken) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - if (subIDS == null) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - if (subIDS.Length == 0) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add subtitle search parameters. Each one will be like 'array' of structs. - var array = new XmlRpcValueArray(); - foreach (int id in subIDS) - { - array.Values.Add(new XmlRpcValueBasic(id, XmlRpcBasicValueType.Int)); - } - // Add the array to the parameters - parms.Add(array); - // Call ! - var call = new XmlRpcMethodCall("DownloadSubtitles", parms); - OSHConsole.WriteLine("Sending DownloadSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - - var httpResponse = await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken).ConfigureAwait(false); - - string response = Utilities.GetStreamString(httpResponse); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - // We expect Struct of 3 members: - //* the first is status - //* the second is [array of structs, each one includes subtitle file]. - //* the third is [double basic value] represent seconds token by server. - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSubtitleDownload(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Download results:"); - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new SubtitleDownloadResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "idsubtitlefile": result.IdSubtitleFile = (string)submember.Data.Data; break; - case "data": result.Data = (string)submember.Data.Data; break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine("> IDSubtilteFile= " + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "DownloadSubtitles call failed !"); - } - - /// - /// Returns comments for subtitles - /// - /// The subtitle IDS (an array of IDSubtitleFile value that given by server as SearchSubtiles results) - /// Status of the call operation. If the call success the response will be 'MethodResponseGetComments' - public static IMethodResponse GetComments(int[] subIDS) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - if (subIDS == null) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - if (subIDS.Length == 0) - { - OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); - return new MethodResponseError("Fail", "No subtitle id passed"); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN)); - // Add subtitle search parameters. Each one will be like 'array' of structs. - var array = new XmlRpcValueArray(subIDS); - // Add the array to the parameters - parms.Add(array); - // Call ! - var call = new XmlRpcMethodCall("GetComments", parms); - OSHConsole.WriteLine("Sending GetComments request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseGetComments(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Comments results:"); - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue commentStruct in rarray.Values) - { - if (commentStruct == null) continue; - if (!(commentStruct is XmlRpcValueStruct)) continue; - - var result = new GetCommentsResult(); - foreach (XmlRpcStructMember commentmember in ((XmlRpcValueStruct)commentStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (commentmember.Name) - { - case "IDSubtitle": result.IDSubtitle = (string)commentmember.Data.Data; break; - case "UserID": result.UserID = (string)commentmember.Data.Data; break; - case "UserNickName": result.UserNickName = (string)commentmember.Data.Data; break; - case "Comment": result.Comment = (string)commentmember.Data.Data; break; - case "Created": result.Created = (string)commentmember.Data.Data; break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine("> IDSubtitle= " + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "GetComments call failed !"); - } - - /// - /// Schedule a periodical search for subtitles matching given video files, send results to user's e-mail address. - /// - /// The language 3 lenght ids array - /// The movies parameters - /// Status of the call operation. If the call success the response will be 'MethodResponseSearchToMail' - public static IMethodResponse SearchToMail(string[] languageIDS, SearchToMailMovieParameter[] movies) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Array of sub langs - var a = new XmlRpcValueArray(languageIDS); - parms.Add(a); - // Array of video parameters - a = new XmlRpcValueArray(); - foreach (SearchToMailMovieParameter p in movies) - { - var str = new XmlRpcValueStruct(new List()); - str.Members.Add(new XmlRpcStructMember("moviehash", new XmlRpcValueBasic(p.moviehash))); - str.Members.Add(new XmlRpcStructMember("moviesize", new XmlRpcValueBasic(p.moviesize))); - a.Values.Add(str); - } - parms.Add(a); - var call = new XmlRpcMethodCall("SearchToMail", parms); - - OSHConsole.WriteLine("Sending SearchToMail request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSearchToMail(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "SearchToMail call failed !"); - } - /*Movies*/ - /// - /// Search for a movie (using movie title) - /// - /// Movie title user is searching for, this is cleaned-up a bit (remove dvdrip, etc.) before searching - /// Status of the call operation. If the call success the response will be 'MethodResponseSubtitleSearch' - public static IMethodResponse SearchMoviesOnIMDB(string query) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add query param - parms.Add(new XmlRpcValueBasic(query, XmlRpcBasicValueType.String)); - // Call ! - var call = new XmlRpcMethodCall("SearchMoviesOnIMDB", parms); - OSHConsole.WriteLine("Sending SearchMoviesOnIMDB request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseMovieSearch(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Search results:"); - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new MovieSearchResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "id": result.ID = (string)submember.Data.Data; break; - case "title": result.Title = (string)submember.Data.Data; break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine(">" + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "SearchMoviesOnIMDB call failed !"); - } - /// - /// Get movie details for given IMDb ID - /// - /// http://www.imdb.com/ - /// Status of the call operation. If the call success the response will be 'MethodResponseMovieDetails' - public static IMethodResponse GetIMDBMovieDetails(string imdbid) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN)); - // Add query param - parms.Add(new XmlRpcValueBasic(imdbid)); - // Call ! - var call = new XmlRpcMethodCall("GetIMDBMovieDetails", parms); - OSHConsole.WriteLine("Sending GetIMDBMovieDetails request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseMovieDetails(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "data") - { - // We expect struct with details... - if (MEMBER.Data is XmlRpcValueStruct) - { - OSHConsole.WriteLine("Details result:"); - var detailsStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dmem in detailsStruct.Members) - { - switch (dmem.Name) - { - case "id": R.ID = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "title": R.Title = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "year": R.Year = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "cover": R.CoverLink = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "duration": R.Duration = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "tagline": R.Tagline = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "plot": R.Plot = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "goofs": R.Goofs = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "trivia": R.Trivia = dmem.Data.Data.ToString(); OSHConsole.WriteLine(">" + dmem.Name + "= " + dmem.Data.Data.ToString()); break; - case "cast": - // this is another struct with cast members... - OSHConsole.WriteLine(">" + dmem.Name + "= "); - var castStruct = (XmlRpcValueStruct)dmem.Data; - foreach (XmlRpcStructMember castMemeber in castStruct.Members) - { - R.Cast.Add(castMemeber.Data.Data.ToString()); - OSHConsole.WriteLine(" >" + castMemeber.Data.Data.ToString()); - } - break; - case "directors": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is another struct with directors members... - var directorsStruct = (XmlRpcValueStruct)dmem.Data; - foreach (XmlRpcStructMember directorsMember in directorsStruct.Members) - { - R.Directors.Add(directorsMember.Data.Data.ToString()); - OSHConsole.WriteLine(" >" + directorsMember.Data.Data.ToString()); - } - break; - case "writers": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is another struct with writers members... - var writersStruct = (XmlRpcValueStruct)dmem.Data; - foreach (XmlRpcStructMember writersMember in writersStruct.Members) - { - R.Writers.Add(writersMember.Data.Data.ToString()); - OSHConsole.WriteLine("+->" + writersMember.Data.Data.ToString()); - } - break; - case "awards": - // this is an array of genres... - var awardsArray = (XmlRpcValueArray)dmem.Data; - foreach (XmlRpcValueBasic award in awardsArray.Values) - { - R.Awards.Add(award.Data.ToString()); - OSHConsole.WriteLine(" >" + award.Data.ToString()); - } - break; - case "genres": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is an array of genres... - var genresArray = (XmlRpcValueArray)dmem.Data; - foreach (XmlRpcValueBasic genre in genresArray.Values) - { - R.Genres.Add(genre.Data.ToString()); - OSHConsole.WriteLine(" >" + genre.Data.ToString()); - } - break; - case "country": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is an array of country... - var countryArray = (XmlRpcValueArray)dmem.Data; - foreach (XmlRpcValueBasic country in countryArray.Values) - { - R.Country.Add(country.Data.ToString()); - OSHConsole.WriteLine(" >" + country.Data.ToString()); - } - break; - case "language": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is an array of language... - var languageArray = (XmlRpcValueArray)dmem.Data; - foreach (XmlRpcValueBasic language in languageArray.Values) - { - R.Language.Add(language.Data.ToString()); - OSHConsole.WriteLine(" >" + language.Data.ToString()); - } - break; - case "certification": - OSHConsole.WriteLine(">" + dmem.Name + "= "); - // this is an array of certification... - var certificationArray = (XmlRpcValueArray)dmem.Data; - foreach (XmlRpcValueBasic certification in certificationArray.Values) - { - R.Certification.Add(certification.Data.ToString()); - OSHConsole.WriteLine(" >" + certification.Data.ToString()); - } - break; - } - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "GetIMDBMovieDetails call failed !"); - } - /// - /// Allows registered users to insert new movies (not stored in IMDb) to the database. - /// - /// Movie title - /// Release year - /// Status of the call operation. If the call success the response will be 'MethodResponseInsertMovie' - public static IMethodResponse InsertMovie(string movieName, string movieyear) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - // Add token param - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // Add movieinfo struct - var movieinfo = new XmlRpcValueStruct(new List()); - movieinfo.Members.Add(new XmlRpcStructMember("moviename", new XmlRpcValueBasic(movieName))); - movieinfo.Members.Add(new XmlRpcStructMember("movieyear", new XmlRpcValueBasic(movieyear))); - parms.Add(movieinfo); - // Call ! - var call = new XmlRpcMethodCall("InsertMovie", parms); - OSHConsole.WriteLine("Sending InsertMovie request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseInsertMovie(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - if (MEMBER.Name == "status") - { - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("Status= " + R.Status); - } - else if (MEMBER.Name == "seconds") - { - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine("Seconds= " + R.Seconds); - } - else if (MEMBER.Name == "id") - { - R.ID = (string)MEMBER.Data.Data; - OSHConsole.WriteLine("ID= " + R.Seconds); - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "InsertMovie call failed !"); - } - /// - /// Inserts or updates data to tables, which are used for CheckMovieHash() and !CheckMovieHash2(). - /// - /// The parameters - /// Status of the call operation. If the call success the response will be 'MethodResponseInsertMovieHash' - public static IMethodResponse InsertMovieHash(InsertMovieHashParameters[] parameters) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - foreach (InsertMovieHashParameters p in parameters) - { - var pstruct = new XmlRpcValueStruct(new List()); - pstruct.Members.Add(new XmlRpcStructMember("moviehash", new XmlRpcValueBasic(p.moviehash))); - pstruct.Members.Add(new XmlRpcStructMember("moviebytesize", new XmlRpcValueBasic(p.moviebytesize))); - pstruct.Members.Add(new XmlRpcStructMember("imdbid", new XmlRpcValueBasic(p.imdbid))); - pstruct.Members.Add(new XmlRpcStructMember("movietimems", new XmlRpcValueBasic(p.movietimems))); - pstruct.Members.Add(new XmlRpcStructMember("moviefps", new XmlRpcValueBasic(p.moviefps))); - pstruct.Members.Add(new XmlRpcStructMember("moviefilename", new XmlRpcValueBasic(p.moviefilename))); - parms.Add(pstruct); - } - var call = new XmlRpcMethodCall("InsertMovieHash", parms); - - OSHConsole.WriteLine("Sending InsertMovieHash request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseInsertMovieHash(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "seconds": - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - switch (dataMember.Name) - { - case "accepted_moviehashes": - var mh = (XmlRpcValueArray)dataMember.Data; - foreach (IXmlRpcValue val in mh.Values) - { - if (val is XmlRpcValueBasic) - { - R.accepted_moviehashes.Add(val.Data.ToString()); - } - } - break; - case "new_imdbs": - var mi = (XmlRpcValueArray)dataMember.Data; - foreach (IXmlRpcValue val in mi.Values) - { - if (val is XmlRpcValueBasic) - { - R.new_imdbs.Add(val.Data.ToString()); - } - } - break; - } - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "InsertMovieHash call failed !"); - } - /*Reporting and rating*/ - /// - /// Get basic server information and statistics - /// - /// Status of the call operation. If the call success the response will be 'MethodResponseServerInfo' - public static IMethodResponse ServerInfo() - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT, XmlRpcBasicValueType.String)); - var call = new XmlRpcMethodCall("ServerInfo", parms); - - OSHConsole.WriteLine("Sending ServerInfo request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseServerInfo(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "seconds": - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "xmlrpc_version": - R.xmlrpc_version = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "xmlrpc_url": - R.xmlrpc_url = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "application": - R.application = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "contact": - R.contact = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "website_url": - R.website_url = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "users_online_total": - R.users_online_total = (int)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "users_online_program": - R.users_online_program = (int)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "users_loggedin": - R.users_loggedin = (int)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "users_max_alltime": - R.users_max_alltime = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "users_registered": - R.users_registered = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "subs_downloads": - R.subs_downloads = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "subs_subtitle_files": - R.subs_subtitle_files = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "movies_total": - R.movies_total = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "movies_aka": - R.movies_aka = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "total_subtitles_languages": - R.total_subtitles_languages = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "last_update_strings": - //R.total_subtitles_languages = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + ":"); - var luStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember luMemeber in luStruct.Members) - { - R.last_update_strings.Add(luMemeber.Name + " [" + luMemeber.Data.Data.ToString() + "]"); - OSHConsole.WriteLine(" >" + luMemeber.Name + "= " + luMemeber.Data.Data.ToString()); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "ServerInfo call failed !"); - } - /// - /// Report wrong subtitle file <--> video file combination - /// - /// Identifier of the subtitle file <--> video file combination - /// Status of the call operation. If the call success the response will be 'MethodResponseReportWrongMovieHash' - public static IMethodResponse ReportWrongMovieHash(string IDSubMovieFile) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - parms.Add(new XmlRpcValueBasic(IDSubMovieFile, XmlRpcBasicValueType.String)); - var call = new XmlRpcMethodCall("ReportWrongMovieHash", parms); - - OSHConsole.WriteLine("Sending ReportWrongMovieHash request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseReportWrongMovieHash(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": - R.Status = (string)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - case "seconds": - R.Seconds = (double)MEMBER.Data.Data; - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "ReportWrongMovieHash call failed !"); - } - /// - /// This method is needed to report bad movie hash for imdbid. This method should be used for correcting wrong entries, - /// when using CheckMovieHash2. Pass moviehash and moviebytesize for file, and imdbid as new, corrected one IMDBID - /// (id number without trailing zeroes). After some reports, moviehash will be linked to new imdbid. - /// - /// The movie hash - /// The movie size in bytes - /// The movie imbid - /// Status of the call operation. If the call success the response will be 'MethodResponseReportWrongImdbMovie' - public static IMethodResponse ReportWrongImdbMovie(string moviehash, string moviebytesize, string imdbid) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var s = new XmlRpcValueStruct(new List()); - s.Members.Add(new XmlRpcStructMember("moviehash", new XmlRpcValueBasic(moviehash))); - s.Members.Add(new XmlRpcStructMember("moviebytesize", new XmlRpcValueBasic(moviebytesize))); - s.Members.Add(new XmlRpcStructMember("imdbid", new XmlRpcValueBasic(imdbid))); - parms.Add(s); - var call = new XmlRpcMethodCall("ReportWrongImdbMovie", parms); - - OSHConsole.WriteLine("Sending ReportWrongImdbMovie request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseAddComment(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "ReportWrongImdbMovie call failed !"); - } - /// - /// Rate subtitles - /// - /// Id of subtitle (NOT subtitle file) user wants to rate - /// Subtitle rating, must be in interval 1 (worst) to 10 (best). - /// Status of the call operation. If the call success the response will be 'MethodResponseSubtitlesVote' - public static IMethodResponse SubtitlesVote(int idsubtitle, int score) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var s = new XmlRpcValueStruct(new List()); - s.Members.Add(new XmlRpcStructMember("idsubtitle", new XmlRpcValueBasic(idsubtitle))); - s.Members.Add(new XmlRpcStructMember("score", new XmlRpcValueBasic(score))); - parms.Add(s); - var call = new XmlRpcMethodCall("SubtitlesVote", parms); - - OSHConsole.WriteLine("Sending SubtitlesVote request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseSubtitlesVote(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dataMemeber in dataStruct.Members) - { - OSHConsole.WriteLine(" >" + dataMemeber.Name + "= " + dataMemeber.Data.Data.ToString()); - switch (dataMemeber.Name) - { - case "SubRating": R.SubRating = dataMemeber.Data.Data.ToString(); break; - case "SubSumVotes": R.SubSumVotes = dataMemeber.Data.Data.ToString(); break; - case "IDSubtitle": R.IDSubtitle = dataMemeber.Data.Data.ToString(); break; - } - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "SubtitlesVote call failed !"); - } - /// - /// Add comment to a subtitle - /// - /// Subtitle identifier (BEWARE! this is not the ID of subtitle file but of the whole subtitle (a subtitle can contain multiple subtitle files)) - /// User's comment - /// Optional parameter. If set to 1, subtitles are marked as bad. - /// Status of the call operation. If the call success the response will be 'MethodResponseAddComment' - public static IMethodResponse AddComment(int idsubtitle, string comment, int badsubtitle) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var s = new XmlRpcValueStruct(new List()); - s.Members.Add(new XmlRpcStructMember("idsubtitle", new XmlRpcValueBasic(idsubtitle))); - s.Members.Add(new XmlRpcStructMember("comment", new XmlRpcValueBasic(comment))); - s.Members.Add(new XmlRpcStructMember("badsubtitle", new XmlRpcValueBasic(badsubtitle))); - parms.Add(s); - var call = new XmlRpcMethodCall("AddComment", parms); - - OSHConsole.WriteLine("Sending AddComment request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseAddComment(); - - // To make sure response is not currepted by server, do it in loop - foreach (var MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "AddComment call failed !"); - } - /// - /// Add new request for subtitles, user must be logged in - /// - /// The subtitle language id 3 length - /// http://www.imdb.com/ - /// The comment - /// Status of the call operation. If the call success the response will be 'MethodResponseAddRequest' - public static IMethodResponse AddRequest(string sublanguageid, string idmovieimdb, string comment) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var s = new XmlRpcValueStruct(new List()); - s.Members.Add(new XmlRpcStructMember("sublanguageid", new XmlRpcValueBasic(sublanguageid))); - s.Members.Add(new XmlRpcStructMember("idmovieimdb", new XmlRpcValueBasic(idmovieimdb))); - s.Members.Add(new XmlRpcStructMember("comment", new XmlRpcValueBasic(comment))); - parms.Add(s); - var call = new XmlRpcMethodCall("AddRequest", parms); - - OSHConsole.WriteLine("Sending AddRequest request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseAddRequest(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dataMemeber in dataStruct.Members) - { - switch (dataMemeber.Name) - { - case "request_url": R.request_url = dataMemeber.Data.Data.ToString(); OSHConsole.WriteLine(">" + dataMemeber.Name + "= " + dataMemeber.Data.Data.ToString()); break; - } - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "AddRequest call failed !"); - } - /*User interface*/ - /// - /// Get list of supported subtitle languages - /// - /// ISO639-1 2-letter language code of user's interface language. - /// Status of the call operation. If the call success the response will be 'MethodResponseGetSubLanguages' - public static IMethodResponse GetSubLanguages(string language) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueBasic(language)); - var call = new XmlRpcMethodCall("GetSubLanguages", parms); - - OSHConsole.WriteLine("Sending GetSubLanguages request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseGetSubLanguages(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data":// array of structs - var array = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue value in array.Values) - { - if (value is XmlRpcValueStruct) - { - var valueStruct = (XmlRpcValueStruct)value; - var lang = new SubtitleLanguage(); - OSHConsole.WriteLine(">SubLanguage:"); - foreach (XmlRpcStructMember langMemeber in valueStruct.Members) - { - OSHConsole.WriteLine(" >" + langMemeber.Name + "= " + langMemeber.Data.Data.ToString()); - switch (langMemeber.Name) - { - case "SubLanguageID": lang.SubLanguageID = langMemeber.Data.Data.ToString(); break; - case "LanguageName": lang.LanguageName = langMemeber.Data.Data.ToString(); break; - case "ISO639": lang.ISO639 = langMemeber.Data.Data.ToString(); break; - } - } - R.Languages.Add(lang); - } - else - { - OSHConsole.WriteLine(">" + MEMBER.Name + "= " + - MEMBER.Data.Data.ToString() + " [Struct expected !]", DebugCode.Warning); - } - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "GetSubLanguages call failed !"); - } - /// - /// Detect language for given strings - /// - /// Array of strings you want language detected for - /// The encoding that will be used to get buffer of given strings. (this is not OS official parameter) - /// Status of the call operation. If the call success the response will be 'MethodResponseDetectLanguage' - public static IMethodResponse DetectLanguage(string[] texts, Encoding encodingUsed) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - // We need to gzip texts then code them with base 24 - var decodedTexts = new List(); - foreach (string text in texts) - { - // compress - Stream str = new MemoryStream(); - byte[] stringData = encodingUsed.GetBytes(text); - str.Write(stringData, 0, stringData.Length); - str.Position = 0; - byte[] data = Utilities.Compress(str); - //base 64 - decodedTexts.Add(Convert.ToBase64String(data)); - } - parms.Add(new XmlRpcValueArray(decodedTexts.ToArray())); - var call = new XmlRpcMethodCall("DetectLanguage", parms); - - OSHConsole.WriteLine("Sending DetectLanguage request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseDetectLanguage(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - if (MEMBER.Data is XmlRpcValueStruct) - { - OSHConsole.WriteLine(">Languages:"); - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - var lang = new DetectLanguageResult(); - lang.InputSample = dataMember.Name; - lang.LanguageID = dataMember.Data.Data.ToString(); - R.Results.Add(lang); - OSHConsole.WriteLine(" >" + dataMember.Name + " (" + dataMember.Data.Data.ToString() + ")"); - } - } - else - { - OSHConsole.WriteLine(">Languages ?? Struct expected but server return another type!!", DebugCode.Warning); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "DetectLanguage call failed !"); - } - /// - /// Get available translations for given program - /// - /// Name of the program/client application you want translations for. Currently supported values: subdownloader, oscar - /// Status of the call operation. If the call success the response will be 'MethodResponseGetAvailableTranslations' - public static IMethodResponse GetAvailableTranslations(string program) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueBasic(program)); - var call = new XmlRpcMethodCall("GetAvailableTranslations", parms); - - OSHConsole.WriteLine("Sending GetAvailableTranslations request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseGetAvailableTranslations(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - OSHConsole.WriteLine(">data:"); - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - if (dataMember.Data is XmlRpcValueStruct) - { - var resStruct = (XmlRpcValueStruct)dataMember.Data; - var res = new GetAvailableTranslationsResult(); - res.LanguageID = dataMember.Name; - OSHConsole.WriteLine(" >LanguageID: " + dataMember.Name); - foreach (XmlRpcStructMember resMember in resStruct.Members) - { - switch (resMember.Name) - { - case "LastCreated": res.LastCreated = resMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + resMember.Name + "= " + resMember.Data.Data.ToString()); break; - case "StringsNo": res.StringsNo = resMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + resMember.Name + "= " + resMember.Data.Data.ToString()); break; - } - R.Results.Add(res); - } - } - else - { - OSHConsole.WriteLine(" >Struct expected !!", DebugCode.Warning); - } - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "GetAvailableTranslations call failed !"); - } - /// - /// Get a translation for given program and language - /// - /// language ​ISO639-1 2-letter code - /// available formats: [gnugettext compatible: mo, po] and [additional: txt, xml] - /// Name of the program/client application you want translations for. (currently supported values: subdownloader, oscar) - /// Status of the call operation. If the call success the response will be 'MethodResponseGetTranslation' - public static IMethodResponse GetTranslation(string iso639, string format, string program) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueBasic(iso639)); - parms.Add(new XmlRpcValueBasic(format)); - parms.Add(new XmlRpcValueBasic(program)); - var call = new XmlRpcMethodCall("GetTranslation", parms); - - OSHConsole.WriteLine("Sending GetTranslation request to the server ...", DebugCode.Good); - // Send the request to the server - //File.WriteAllText(".\\REQUEST_GetTranslation.xml", Encoding.ASCII.GetString(XmlRpcGenerator.Generate(call))); - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseGetTranslation(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": R.ContentData = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "GetTranslation call failed !"); - } - /// - /// Check for the latest version of given application - /// - /// name of the program/client application you want to check. (Currently supported values: subdownloader, oscar) - /// Status of the call operation. If the call success the response will be 'MethodResponseAutoUpdate' - public static IMethodResponse AutoUpdate(string program) - { - /*if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - }*/ - // Method call .. - var parms = new List(); - // parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueBasic(program)); - // parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT)); - - var call = new XmlRpcMethodCall("AutoUpdate", parms); - OSHConsole.WriteLine("Sending AutoUpdate request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseAutoUpdate(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "version": R.version = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "url_windows": R.url_windows = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "url_linux": R.url_linux = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "comments": R.comments = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "AutoUpdate call failed !"); - } - /*Checking*/ - /// - /// Check if video file hashes are already stored in the database - /// - /// Array of video file hashes - /// Status of the call operation. If the call success the response will be 'MethodResponseCheckMovieHash' - public static IMethodResponse CheckMovieHash(string[] hashes) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueArray(hashes)); - var call = new XmlRpcMethodCall("CheckMovieHash", parms); - - OSHConsole.WriteLine("Sending CheckMovieHash request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseCheckMovieHash(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - OSHConsole.WriteLine(">Data:"); - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - var res = new CheckMovieHashResult(); - res.Name = dataMember.Name; - OSHConsole.WriteLine(" >" + res.Name + ":"); - var movieStruct = (XmlRpcValueStruct)dataMember.Data; - foreach (XmlRpcStructMember movieMember in movieStruct.Members) - { - switch (movieMember.Name) - { - case "MovieHash": res.MovieHash = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieImdbID": res.MovieImdbID = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieName": res.MovieName = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieYear": res.MovieYear = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - } - } - R.Results.Add(res); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "CheckMovieHash call failed !"); - } - /// - /// Check if video file hashes are already stored in the database. This method returns matching !MovieImdbID, MovieName, MovieYear, SeriesSeason, SeriesEpisode, - /// MovieKind if available for each $moviehash, always sorted by SeenCount DESC. - /// - /// Array of video file hashes - /// Status of the call operation. If the call success the response will be 'MethodResponseCheckMovieHash2' - public static IMethodResponse CheckMovieHash2(string[] hashes) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueArray(hashes)); - var call = new XmlRpcMethodCall("CheckMovieHash2", parms); - - OSHConsole.WriteLine("Sending CheckMovieHash2 request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseCheckMovieHash2(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - OSHConsole.WriteLine(">Data:"); - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - var res = new CheckMovieHash2Result(); - res.Name = dataMember.Name; - OSHConsole.WriteLine(" >" + res.Name + ":"); - - var dataArray = (XmlRpcValueArray)dataMember.Data; - foreach (XmlRpcValueStruct movieStruct in dataArray.Values) - { - var d = new CheckMovieHash2Data(); - foreach (XmlRpcStructMember movieMember in movieStruct.Members) - { - switch (movieMember.Name) - { - case "MovieHash": d.MovieHash = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieImdbID": d.MovieImdbID = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieName": d.MovieName = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieYear": d.MovieYear = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "MovieKind": d.MovieKind = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "SeriesSeason": d.SeriesSeason = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "SeriesEpisode": d.SeriesEpisode = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - case "SeenCount": d.MovieYear = movieMember.Data.Data.ToString(); OSHConsole.WriteLine(" >" + movieMember.Name + "= " + movieMember.Data.Data.ToString()); break; - } - } - res.Items.Add(d); - } - R.Results.Add(res); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "CheckMovieHash2 call failed !"); - } - /// - /// Check if given subtitle files are already stored in the database - /// - /// Array of subtitle file hashes (MD5 hashes of subtitle file contents) - /// Status of the call operation. If the call success the response will be 'MethodResponseCheckSubHash' - public static IMethodResponse CheckSubHash(string[] hashes) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - parms.Add(new XmlRpcValueArray(hashes)); - var call = new XmlRpcMethodCall("CheckSubHash", parms); - - OSHConsole.WriteLine("Sending CheckSubHash request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseCheckSubHash(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - OSHConsole.WriteLine(">Data:"); - var dataStruct = (XmlRpcValueStruct)MEMBER.Data; - foreach (XmlRpcStructMember dataMember in dataStruct.Members) - { - OSHConsole.WriteLine(" >" + dataMember.Name + "= " + dataMember.Data.Data.ToString()); - var r = new CheckSubHashResult(); - r.Hash = dataMember.Name; - r.SubID = dataMember.Data.Data.ToString(); - R.Results.Add(r); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "CheckSubHash call failed !"); - } - /*Upload*/ - /// - /// Try to upload subtitles, perform pre-upload checking (i.e. check if subtitles already exist on server) - /// - /// The subtitle parameters collection to try to upload - /// Status of the call operation. If the call success the response will be 'MethodResponseTryUploadSubtitles' - public static IMethodResponse TryUploadSubtitles(TryUploadSubtitlesParameters[] subs) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); - var s = new XmlRpcValueStruct(new List()); - int i = 1; - foreach (var cd in subs) - { - var member = new XmlRpcStructMember("cd" + i, null); - var memberStruct = new XmlRpcValueStruct(new List()); - memberStruct.Members.Add(new XmlRpcStructMember("subhash", new XmlRpcValueBasic(cd.subhash))); - memberStruct.Members.Add(new XmlRpcStructMember("subfilename", new XmlRpcValueBasic(cd.subfilename))); - memberStruct.Members.Add(new XmlRpcStructMember("moviehash", new XmlRpcValueBasic(cd.moviehash))); - memberStruct.Members.Add(new XmlRpcStructMember("moviebytesize", new XmlRpcValueBasic(cd.moviebytesize))); - memberStruct.Members.Add(new XmlRpcStructMember("moviefps", new XmlRpcValueBasic(cd.moviefps))); - memberStruct.Members.Add(new XmlRpcStructMember("movietimems", new XmlRpcValueBasic(cd.movietimems))); - memberStruct.Members.Add(new XmlRpcStructMember("movieframes", new XmlRpcValueBasic(cd.movieframes))); - memberStruct.Members.Add(new XmlRpcStructMember("moviefilename", new XmlRpcValueBasic(cd.moviefilename))); - member.Data = memberStruct; - s.Members.Add(member); - i++; - } - parms.Add(s); - var call = new XmlRpcMethodCall("TryUploadSubtitles", parms); - - OSHConsole.WriteLine("Sending TryUploadSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseTryUploadSubtitles(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "alreadyindb": R.AlreadyInDB = (int)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": - if (MEMBER.Data is XmlRpcValueArray) - { - OSHConsole.WriteLine("Results: "); - - var rarray = (XmlRpcValueArray)MEMBER.Data; - foreach (IXmlRpcValue subStruct in rarray.Values) - { - if (subStruct == null) continue; - if (!(subStruct is XmlRpcValueStruct)) continue; - - var result = new SubtitleSearchResult(); - foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) - { - // To avoid errors of arranged info or missing ones, let's do it with switch.. - switch (submember.Name) - { - case "IDMovie": result.IDMovie = submember.Data.Data.ToString(); break; - case "IDMovieImdb": result.IDMovieImdb = submember.Data.Data.ToString(); break; - case "IDSubMovieFile": result.IDSubMovieFile = submember.Data.Data.ToString(); break; - case "IDSubtitle": result.IDSubtitle = submember.Data.Data.ToString(); break; - case "IDSubtitleFile": result.IDSubtitleFile = submember.Data.Data.ToString(); break; - case "ISO639": result.ISO639 = submember.Data.Data.ToString(); break; - case "LanguageName": result.LanguageName = submember.Data.Data.ToString(); break; - case "MovieByteSize": result.MovieByteSize = submember.Data.Data.ToString(); break; - case "MovieHash": result.MovieHash = submember.Data.Data.ToString(); break; - case "MovieImdbRating": result.MovieImdbRating = submember.Data.Data.ToString(); break; - case "MovieName": result.MovieName = submember.Data.Data.ToString(); break; - case "MovieNameEng": result.MovieNameEng = submember.Data.Data.ToString(); break; - case "MovieReleaseName": result.MovieReleaseName = submember.Data.Data.ToString(); break; - case "MovieTimeMS": result.MovieTimeMS = submember.Data.Data.ToString(); break; - case "MovieYear": result.MovieYear = submember.Data.Data.ToString(); break; - case "SubActualCD": result.SubActualCD = submember.Data.Data.ToString(); break; - case "SubAddDate": result.SubAddDate = submember.Data.Data.ToString(); break; - case "SubAuthorComment": result.SubAuthorComment = submember.Data.Data.ToString(); break; - case "SubBad": result.SubBad = submember.Data.Data.ToString(); break; - case "SubDownloadLink": result.SubDownloadLink = submember.Data.Data.ToString(); break; - case "SubDownloadsCnt": result.SubDownloadsCnt = submember.Data.Data.ToString(); break; - case "SubFileName": result.SubFileName = submember.Data.Data.ToString(); break; - case "SubFormat": result.SubFormat = submember.Data.Data.ToString(); break; - case "SubHash": result.SubHash = submember.Data.Data.ToString(); break; - case "SubLanguageID": result.SubLanguageID = submember.Data.Data.ToString(); break; - case "SubRating": result.SubRating = submember.Data.Data.ToString(); break; - case "SubSize": result.SubSize = submember.Data.Data.ToString(); break; - case "SubSumCD": result.SubSumCD = submember.Data.Data.ToString(); break; - case "UserID": result.UserID = submember.Data.Data.ToString(); break; - case "UserNickName": result.UserNickName = submember.Data.Data.ToString(); break; - case "ZipDownloadLink": result.ZipDownloadLink = submember.Data.Data.ToString(); break; - } - } - R.Results.Add(result); - OSHConsole.WriteLine(">" + result.ToString()); - } - } - else// Unknown data ? - { - OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); - } - break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "TryUploadSubtitles call failed !"); - } - /// - /// Upload given subtitles to OSDb server - /// - /// The pamaters of upload method - /// Status of the call operation. If the call success the response will be 'MethodResponseUploadSubtitles' - public static IMethodResponse UploadSubtitles(UploadSubtitleInfoParameters info) - { - if (TOKEN == "") - { - OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); - return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); - } - // Method call .. - var parms = new List(); - parms.Add(new XmlRpcValueBasic(TOKEN)); - // Main struct - var s = new XmlRpcValueStruct(new List()); - - // Base info member as struct - var member = new XmlRpcStructMember("baseinfo", null); - var memberStruct = new XmlRpcValueStruct(new List()); - memberStruct.Members.Add(new XmlRpcStructMember("idmovieimdb", new XmlRpcValueBasic(info.idmovieimdb))); - memberStruct.Members.Add(new XmlRpcStructMember("sublanguageid", new XmlRpcValueBasic(info.sublanguageid))); - memberStruct.Members.Add(new XmlRpcStructMember("moviereleasename", new XmlRpcValueBasic(info.moviereleasename))); - memberStruct.Members.Add(new XmlRpcStructMember("movieaka", new XmlRpcValueBasic(info.movieaka))); - memberStruct.Members.Add(new XmlRpcStructMember("subauthorcomment", new XmlRpcValueBasic(info.subauthorcomment))); - // memberStruct.Members.Add(new XmlRpcStructMember("hearingimpaired", new XmlRpcValueBasic(info.hearingimpaired))); - // memberStruct.Members.Add(new XmlRpcStructMember("highdefinition", new XmlRpcValueBasic(info.highdefinition))); - // memberStruct.Members.Add(new XmlRpcStructMember("automatictranslation", new XmlRpcValueBasic(info.automatictranslation))); - member.Data = memberStruct; - s.Members.Add(member); - - // CDS members - int i = 1; - foreach (UploadSubtitleParameters cd in info.CDS) - { - var member2 = new XmlRpcStructMember("cd" + i, null); - var memberStruct2 = new XmlRpcValueStruct(new List()); - memberStruct2.Members.Add(new XmlRpcStructMember("subhash", new XmlRpcValueBasic(cd.subhash))); - memberStruct2.Members.Add(new XmlRpcStructMember("subfilename", new XmlRpcValueBasic(cd.subfilename))); - memberStruct2.Members.Add(new XmlRpcStructMember("moviehash", new XmlRpcValueBasic(cd.moviehash))); - memberStruct2.Members.Add(new XmlRpcStructMember("moviebytesize", new XmlRpcValueBasic(cd.moviebytesize))); - memberStruct2.Members.Add(new XmlRpcStructMember("moviefps", new XmlRpcValueBasic(cd.moviefps))); - memberStruct2.Members.Add(new XmlRpcStructMember("movietimems", new XmlRpcValueBasic(cd.movietimems))); - memberStruct2.Members.Add(new XmlRpcStructMember("movieframes", new XmlRpcValueBasic(cd.movieframes))); - memberStruct2.Members.Add(new XmlRpcStructMember("moviefilename", new XmlRpcValueBasic(cd.moviefilename))); - memberStruct2.Members.Add(new XmlRpcStructMember("subcontent", new XmlRpcValueBasic(cd.subcontent))); - member2.Data = memberStruct2; - s.Members.Add(member2); - i++; - } - - // add main struct to parameters - parms.Add(s); - // add user agent - //parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT)); - var call = new XmlRpcMethodCall("UploadSubtitles", parms); - OSHConsole.WriteLine("Sending UploadSubtitles request to the server ...", DebugCode.Good); - // Send the request to the server - string response = Utilities.GetStreamString(Utilities.SendRequest(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT)); - if (!response.Contains("ERROR:")) - { - // No error occur, get and decode the response. - XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); - if (calls.Length > 0) - { - if (calls[0].Parameters.Count > 0) - { - var mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; - // Create the response, we'll need it later - var R = new MethodResponseUploadSubtitles(); - - // To make sure response is not currepted by server, do it in loop - foreach (XmlRpcStructMember MEMBER in mainStruct.Members) - { - switch (MEMBER.Name) - { - case "status": R.Status = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "seconds": R.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "data": R.Data = (string)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - case "subtitles": R.SubTitles = (bool)MEMBER.Data.Data; OSHConsole.WriteLine(">" + MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break; - } - } - // Return the response to user !! - return R; - } - } - } - else - { - OSHConsole.WriteLine(response, DebugCode.Error); - return new MethodResponseError("Fail", response); - } - return new MethodResponseError("Fail", "UploadSubtitles call failed !"); - } - } -} diff --git a/OpenSubtitlesHandler/OpenSubtitlesHandler.csproj b/OpenSubtitlesHandler/OpenSubtitlesHandler.csproj deleted file mode 100644 index eabd3e070c..0000000000 --- a/OpenSubtitlesHandler/OpenSubtitlesHandler.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - netstandard2.0 - false - - - diff --git a/OpenSubtitlesHandler/OtherTypes/GetAvailableTranslationsResult.cs b/OpenSubtitlesHandler/OtherTypes/GetAvailableTranslationsResult.cs deleted file mode 100644 index 39d0485459..0000000000 --- a/OpenSubtitlesHandler/OtherTypes/GetAvailableTranslationsResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - public struct GetAvailableTranslationsResult - { - private string _language; - private string _LastCreated; - private string _StringsNo; - - public string LanguageID { get { return _language; } set { _language = value; } } - public string LastCreated { get { return _LastCreated; } set { _LastCreated = value; } } - public string StringsNo { get { return _StringsNo; } set { _StringsNo = value; } } - /// - /// LanguageID (LastCreated) - /// - /// - public override string ToString() - { - return _language + " (" + _LastCreated + ")"; - } - } -} diff --git a/OpenSubtitlesHandler/OtherTypes/GetCommentsResult.cs b/OpenSubtitlesHandler/OtherTypes/GetCommentsResult.cs deleted file mode 100644 index 8f4aa9db42..0000000000 --- a/OpenSubtitlesHandler/OtherTypes/GetCommentsResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct GetCommentsResult - { - private string _IDSubtitle; - private string _UserID; - private string _UserNickName; - private string _Comment; - private string _Created; - - public string IDSubtitle { get { return _IDSubtitle; } set { _IDSubtitle = value; } } - public string UserID { get { return _UserID; } set { _UserID = value; } } - public string UserNickName { get { return _UserNickName; } set { _UserNickName = value; } } - public string Comment { get { return _Comment; } set { _Comment = value; } } - public string Created { get { return _Created; } set { _Created = value; } } - } -} diff --git a/OpenSubtitlesHandler/Properties/AssemblyInfo.cs b/OpenSubtitlesHandler/Properties/AssemblyInfo.cs deleted file mode 100644 index b5ae23021c..0000000000 --- a/OpenSubtitlesHandler/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,24 +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("OpenSubtitlesHandler")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] -[assembly: AssemblyCopyright("Copyright © 2013 Ala Ibrahim Hadid. 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.3.0")] -[assembly: AssemblyFileVersion("2019.1.20.3")] diff --git a/OpenSubtitlesHandler/Readme.txt b/OpenSubtitlesHandler/Readme.txt deleted file mode 100644 index d5814aec16..0000000000 --- a/OpenSubtitlesHandler/Readme.txt +++ /dev/null @@ -1,20 +0,0 @@ -OpenSubtitlesHandler -==================== -This project is for OpenSubtitles.org integration‏. The point is to allow user to access OpenSubtitles.org database directly -within ASM without the need to open internet browser. -The plan: Implement the "OSDb protocol" http://trac.opensubtitles.org/projects/opensubtitles/wiki/OSDb - -Copyright: -========= -This library ann all its content are written by Ala Ibrahim Hadid. -Copyright © Ala Ibrahim Hadid 2013 -mailto:ahdsoftwares@hotmail.com - -Resources: -========== -* GetHash.dll: this dll is used to compute hash for movie. - For more information please visit http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#C2 - -XML_RPC: -======== -This class is created to generate XML-RPC requests as XML String. All you need is to call XML_RPC.Generate() method. diff --git a/OpenSubtitlesHandler/SubtitleTypes/CheckSubHashResult.cs b/OpenSubtitlesHandler/SubtitleTypes/CheckSubHashResult.cs deleted file mode 100644 index a2fbe87736..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/CheckSubHashResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct CheckSubHashResult - { - private string _hash; - private string _id; - - public string Hash { get { return _hash; } set { _hash = value; } } - public string SubID { get { return _id; } set { _id = value; } } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/SubtitleDownloadResult.cs b/OpenSubtitlesHandler/SubtitleTypes/SubtitleDownloadResult.cs deleted file mode 100644 index e4194994c3..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/SubtitleDownloadResult.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct SubtitleDownloadResult - { - private string idsubtitlefile; - private string data; - - public string IdSubtitleFile - { get { return idsubtitlefile; } set { idsubtitlefile = value; } } - /// - /// Get or set the data of subtitle file. To decode, decode the string to base64 and then decompress with GZIP. - /// - public string Data - { get { return data; } set { data = value; } } - /// - /// IdSubtitleFile - /// - /// - public override string ToString() - { - return idsubtitlefile.ToString(); - } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/SubtitleLanguage.cs b/OpenSubtitlesHandler/SubtitleTypes/SubtitleLanguage.cs deleted file mode 100644 index b2dc154133..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/SubtitleLanguage.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace OpenSubtitlesHandler -{ - public struct SubtitleLanguage - { - private string _SubLanguageID; - private string _LanguageName; - private string _ISO639; - - public string SubLanguageID { get { return _SubLanguageID; } set { _SubLanguageID = value; } } - public string LanguageName { get { return _LanguageName; } set { _LanguageName = value; } } - public string ISO639 { get { return _ISO639; } set { _ISO639 = value; } } - /// - /// LanguageName [SubLanguageID] - /// - /// LanguageName [SubLanguageID] - public override string ToString() - { - return _LanguageName + " [" + _SubLanguageID + "]"; - } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchParameters.cs b/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchParameters.cs deleted file mode 100644 index 5c8f8c01a4..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchParameters.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - /// - /// Paramaters for subtitle search call - /// - public struct SubtitleSearchParameters - { - public SubtitleSearchParameters(string subLanguageId, string query = "", string season = "", string episode = "", string movieHash = "", long movieByteSize = 0, string imdbid = "") - { - this.subLanguageId = subLanguageId; - this.movieHash = movieHash; - this.movieByteSize = movieByteSize; - this.imdbid = imdbid; - this._episode = episode; - this._season = season; - this._query = query; - } - - private string subLanguageId; - private string movieHash; - private long movieByteSize; - private string imdbid; - private string _query; - private string _episode; - - public string Episode - { - get { return _episode; } - set { _episode = value; } - } - - public string Season - { - get { return _season; } - set { _season = value; } - } - - private string _season; - - public string Query - { - get { return _query; } - set { _query = value; } - } - - /// - /// List of language ISO639-3 language codes to search for, divided by ',' (e.g. 'cze,eng,slo') - /// - public string SubLangaugeID { get { return subLanguageId; } set { subLanguageId = value; } } - /// - /// Video file hash as calculated by one of the implementation functions as seen on http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes - /// - public string MovieHash { get { return movieHash; } set { movieHash = value; } } - /// - /// Size of video file in bytes - /// - public long MovieByteSize { get { return movieByteSize; } set { movieByteSize = value; } } - /// - /// ​IMDb ID of movie this video is part of, belongs to. - /// - public string IMDbID { get { return imdbid; } set { imdbid = value; } } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchResult.cs b/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchResult.cs deleted file mode 100644 index a4a8dd3e69..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/SubtitleSearchResult.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - /// - /// The subtitle search result that comes with server response on SearchSubtitles successed call - /// - public struct SubtitleSearchResult - { - private string _IDSubMovieFile; - private string _MovieHash; - private string _MovieByteSize; - private string _MovieTimeMS; - private string _IDSubtitleFile; - private string _SubFileName; - private string _SubActualCD; - private string _SubSize; - private string _SubHash; - private string _IDSubtitle; - private string _UserID; - private string _SubLanguageID; - private string _SubFormat; - private string _SeriesSeason; - private string _SeriesEpisode; - private string _SubSumCD; - private string _SubAuthorComment; - private string _SubAddDate; - private string _SubBad; - private string _SubRating; - private string _SubDownloadsCnt; - private string _MovieReleaseName; - private string _IDMovie; - private string _IDMovieImdb; - private string _MovieName; - private string _MovieNameEng; - private string _MovieYear; - private string _MovieImdbRating; - private string _UserNickName; - private string _ISO639; - private string _LanguageName; - private string _SubDownloadLink; - private string _ZipDownloadLink; - - public string IDSubMovieFile - { get { return _IDSubMovieFile; } set { _IDSubMovieFile = value; } } - public string MovieHash - { get { return _MovieHash; } set { _MovieHash = value; } } - public string MovieByteSize - { get { return _MovieByteSize; } set { _MovieByteSize = value; } } - public string MovieTimeMS - { get { return _MovieTimeMS; } set { _MovieTimeMS = value; } } - public string IDSubtitleFile - { get { return _IDSubtitleFile; } set { _IDSubtitleFile = value; } } - public string SubFileName - { get { return _SubFileName; } set { _SubFileName = value; } } - public string SubActualCD - { get { return _SubActualCD; } set { _SubActualCD = value; } } - public string SubSize - { get { return _SubSize; } set { _SubSize = value; } } - public string SubHash - { get { return _SubHash; } set { _SubHash = value; } } - public string IDSubtitle - { get { return _IDSubtitle; } set { _IDSubtitle = value; } } - public string UserID - { get { return _UserID; } set { _UserID = value; } } - public string SubLanguageID - { get { return _SubLanguageID; } set { _SubLanguageID = value; } } - public string SubFormat - { get { return _SubFormat; } set { _SubFormat = value; } } - public string SubSumCD - { get { return _SubSumCD; } set { _SubSumCD = value; } } - public string SubAuthorComment - { get { return _SubAuthorComment; } set { _SubAuthorComment = value; } } - public string SubAddDate - { get { return _SubAddDate; } set { _SubAddDate = value; } } - public string SubBad - { get { return _SubBad; } set { _SubBad = value; } } - public string SubRating - { get { return _SubRating; } set { _SubRating = value; } } - public string SubDownloadsCnt - { get { return _SubDownloadsCnt; } set { _SubDownloadsCnt = value; } } - public string MovieReleaseName - { get { return _MovieReleaseName; } set { _MovieReleaseName = value; } } - public string IDMovie - { get { return _IDMovie; } set { _IDMovie = value; } } - public string IDMovieImdb - { get { return _IDMovieImdb; } set { _IDMovieImdb = value; } } - public string MovieName - { get { return _MovieName; } set { _MovieName = value; } } - public string MovieNameEng - { get { return _MovieNameEng; } set { _MovieNameEng = value; } } - public string MovieYear - { get { return _MovieYear; } set { _MovieYear = value; } } - public string MovieImdbRating - { get { return _MovieImdbRating; } set { _MovieImdbRating = value; } } - public string UserNickName - { get { return _UserNickName; } set { _UserNickName = value; } } - public string ISO639 - { get { return _ISO639; } set { _ISO639 = value; } } - public string LanguageName - { get { return _LanguageName; } set { _LanguageName = value; } } - public string SubDownloadLink - { get { return _SubDownloadLink; } set { _SubDownloadLink = value; } } - public string ZipDownloadLink - { get { return _ZipDownloadLink; } set { _ZipDownloadLink = value; } } - public string SeriesSeason - { get { return _SeriesSeason; } set { _SeriesSeason = value; } } - public string SeriesEpisode - { get { return _SeriesEpisode; } set { _SeriesEpisode = value; } } - /// - /// SubFileName + " (" + SubFormat + ")" - /// - /// - public override string ToString() - { - return _SubFileName + " (" + _SubFormat + ")"; - } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/TryUploadSubtitlesParameters.cs b/OpenSubtitlesHandler/SubtitleTypes/TryUploadSubtitlesParameters.cs deleted file mode 100644 index 11f3a706c1..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/TryUploadSubtitlesParameters.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public class TryUploadSubtitlesParameters - { - private string _subhash = ""; - private string _subfilename = ""; - private string _moviehash = ""; - private string _moviebytesize = ""; - private int _movietimems = 0; - private int _movieframes = 0; - private double _moviefps = 0; - private string _moviefilename = ""; - - public string subhash { get { return _subhash; } set { _subhash = value; } } - public string subfilename { get { return _subfilename; } set { _subfilename = value; } } - public string moviehash { get { return _moviehash; } set { _moviehash = value; } } - public string moviebytesize { get { return _moviebytesize; } set { _moviebytesize = value; } } - public int movietimems { get { return _movietimems; } set { _movietimems = value; } } - public int movieframes { get { return _movieframes; } set { _movieframes = value; } } - public double moviefps { get { return _moviefps; } set { _moviefps = value; } } - public string moviefilename { get { return _moviefilename; } set { _moviefilename = value; } } - - public override string ToString() - { - return _subfilename + " (" + _subhash + ")"; - } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleInfoParameter.cs b/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleInfoParameter.cs deleted file mode 100644 index 133cc1d238..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleInfoParameter.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace OpenSubtitlesHandler -{ - public class UploadSubtitleInfoParameters - { - private string _idmovieimdb; - private string _moviereleasename; - private string _movieaka; - private string _sublanguageid; - private string _subauthorcomment; - private bool _hearingimpaired; - private bool _highdefinition; - private bool _automatictranslation; - private List cds; - - public string idmovieimdb { get { return _idmovieimdb; } set { _idmovieimdb = value; } } - public string moviereleasename { get { return _moviereleasename; } set { _moviereleasename = value; } } - public string movieaka { get { return _movieaka; } set { _movieaka = value; } } - public string sublanguageid { get { return _sublanguageid; } set { _sublanguageid = value; } } - public string subauthorcomment { get { return _subauthorcomment; } set { _subauthorcomment = value; } } - public bool hearingimpaired { get { return _hearingimpaired; } set { _hearingimpaired = value; } } - public bool highdefinition { get { return _highdefinition; } set { _highdefinition = value; } } - public bool automatictranslation { get { return _automatictranslation; } set { _automatictranslation = value; } } - public List CDS { get { return cds; } set { cds = value; } } - } -} diff --git a/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleParameters.cs b/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleParameters.cs deleted file mode 100644 index 9910dadc56..0000000000 --- a/OpenSubtitlesHandler/SubtitleTypes/UploadSubtitleParameters.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace OpenSubtitlesHandler -{ - public struct UploadSubtitleParameters - { - private string _subhash; - private string _subfilename; - private string _moviehash; - private double _moviebytesize; - private int _movietimems; - private int _movieframes; - private double _moviefps; - private string _moviefilename; - private string _subcontent; - - public string subhash { get { return _subhash; } set { _subhash = value; } } - public string subfilename { get { return _subfilename; } set { _subfilename = value; } } - public string moviehash { get { return _moviehash; } set { _moviehash = value; } } - public double moviebytesize { get { return _moviebytesize; } set { _moviebytesize = value; } } - public int movietimems { get { return _movietimems; } set { _movietimems = value; } } - public int movieframes { get { return _movieframes; } set { _movieframes = value; } } - public double moviefps { get { return _moviefps; } set { _moviefps = value; } } - public string moviefilename { get { return _moviefilename; } set { _moviefilename = value; } } - /// - /// Sub content. Note: this value must be the subtitle file gziped and then base64 decoded. - /// - public string subcontent { get { return _subcontent; } set { _subcontent = value; } } - - public override string ToString() - { - return _subfilename + " (" + _subhash + ")"; - } - } -} diff --git a/OpenSubtitlesHandler/Utilities.cs b/OpenSubtitlesHandler/Utilities.cs deleted file mode 100644 index 429e28a8f4..0000000000 --- a/OpenSubtitlesHandler/Utilities.cs +++ /dev/null @@ -1,174 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Cryptography; - -namespace OpenSubtitlesHandler -{ - /// - /// Include helper methods. All member are statics. - /// - public sealed class Utilities - { - public static ICryptoProvider CryptographyProvider { get; set; } - public static IHttpClient HttpClient { get; set; } - private static string XML_RPC_SERVER = "https://api.opensubtitles.org/xml-rpc"; - //private static string XML_RPC_SERVER = "https://92.240.234.122/xml-rpc"; - private static string HostHeader = "api.opensubtitles.org:443"; - - /// - /// Compute movie hash - /// - /// The hash as Hexadecimal string - public static string ComputeHash(Stream stream) - { - byte[] hash = MovieHasher.ComputeMovieHash(stream); - return MovieHasher.ToHexadecimal(hash); - } - /// - /// Decompress data using GZip - /// - /// The stream that hold the data - /// Bytes array of decompressed data - public static byte[] Decompress(Stream dataToDecompress) - { - using (var target = new MemoryStream()) - { - using (var decompressionStream = new System.IO.Compression.GZipStream(dataToDecompress, System.IO.Compression.CompressionMode.Decompress)) - { - decompressionStream.CopyTo(target); - } - return target.ToArray(); - } - } - - /// - /// Compress data using GZip (the retunred buffer will be WITHOUT HEADER) - /// - /// The stream that hold the data - /// Bytes array of compressed data WITHOUT HEADER bytes - public static byte[] Compress(Stream dataToCompress) - { - /*using (var compressed = new MemoryStream()) - { - using (var compressor = new System.IO.Compression.GZipStream(compressed, - System.IO.Compression.CompressionMode.Compress)) - { - dataToCompress.CopyTo(compressor); - } - // Get the compressed bytes only after closing the GZipStream - return compressed.ToArray(); - }*/ - //using (var compressedOutput = new MemoryStream()) - //{ - // using (var compressedStream = new ZlibStream(compressedOutput, - // Ionic.Zlib.CompressionMode.Compress, - // CompressionLevel.Default, false)) - // { - // var buffer = new byte[4096]; - // int byteCount; - // do - // { - // byteCount = dataToCompress.Read(buffer, 0, buffer.Length); - - // if (byteCount > 0) - // { - // compressedStream.Write(buffer, 0, byteCount); - // } - // } while (byteCount > 0); - // } - // return compressedOutput.ToArray(); - //} - - throw new NotImplementedException(); - } - - /// - /// Handle server response stream and decode it as given encoding string. - /// - /// The string of the stream after decode using given encoding - public static string GetStreamString(Stream responseStream) - { - using (responseStream) - { - // Handle response, should be XML text. - var data = new List(); - while (true) - { - int r = responseStream.ReadByte(); - if (r < 0) - break; - data.Add((byte)r); - } - var bytes = data.ToArray(); - return Encoding.ASCII.GetString(bytes, 0, bytes.Length); - } - } - - public static byte[] GetASCIIBytes(string text) - { - return Encoding.ASCII.GetBytes(text); - } - - /// - /// Send a request to the server - /// - /// The request buffer to send as bytes array. - /// The user agent value. - /// Response of the server or stream of error message as string started with 'ERROR:' keyword. - public static Stream SendRequest(byte[] request, string userAgent) - { - return SendRequestAsync(request, userAgent, CancellationToken.None).Result; - } - - public static async Task SendRequestAsync(byte[] request, string userAgent, CancellationToken cancellationToken) - { - var options = new HttpRequestOptions - { - RequestContentBytes = request, - RequestContentType = "text/xml", - UserAgent = userAgent, - Host = HostHeader, - Url = XML_RPC_SERVER, - - // Response parsing will fail with this enabled - EnableHttpCompression = false, - - CancellationToken = cancellationToken, - BufferContent = false - }; - - if (string.IsNullOrEmpty(options.UserAgent)) - { - options.UserAgent = "xmlrpc-epi-php/0.2 (PHP)"; - } - - var result = await HttpClient.Post(options).ConfigureAwait(false); - - return result.Content; - } - - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Docs/XML-RPC.txt b/OpenSubtitlesHandler/XML-RPC/Docs/XML-RPC.txt deleted file mode 100644 index a4de38cdea..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Docs/XML-RPC.txt +++ /dev/null @@ -1,225 +0,0 @@ -XML-RPC Specification - -Tue, Jun 15, 1999; by Dave Winer. - -Updated 6/30/03 DW - -Updated 10/16/99 DW - -Updated 1/21/99 DW - -This specification documents the XML-RPC protocol implemented in UserLand Frontier 5.1. - -For a non-technical explanation, see XML-RPC for Newbies. - -This page provides all the information that an implementor needs. - -Overview - -XML-RPC is a Remote Procedure Calling protocol that works over the Internet. - -An XML-RPC message is an HTTP-POST request. The body of the request is in XML. A procedure executes on the server and the value it returns is also formatted in XML. - -Procedure parameters can be scalars, numbers, strings, dates, etc.; and can also be complex record and list structures. - -Request example - -Here's an example of an XML-RPC request: - -POST /RPC2 HTTP/1.0 -User-Agent: Frontier/5.1.2 (WinNT) -Host: betty.userland.com -Content-Type: text/xml -Content-length: 181 - - - - examples.getStateName - - - - 41 - - - - - -Header requirements - -The format of the URI in the first line of the header is not specified. For example, it could be empty, a single slash, if the server is only handling XML-RPC calls. However, if the server is handling a mix of incoming HTTP requests, we allow the URI to help route the request to the code that handles XML-RPC requests. (In the example, the URI is /RPC2, telling the server to route the request to the "RPC2" responder.) - -A User-Agent and Host must be specified. - -The Content-Type is text/xml. - -The Content-Length must be specified and must be correct. - -Payload format - -The payload is in XML, a single structure. - -The must contain a sub-item, a string, containing the name of the method to be called. The string may only contain identifier characters, upper and lower-case A-Z, the numeric characters, 0-9, underscore, dot, colon and slash. It's entirely up to the server to decide how to interpret the characters in a methodName. - -For example, the methodName could be the name of a file containing a script that executes on an incoming request. It could be the name of a cell in a database table. Or it could be a path to a file contained within a hierarchy of folders and files. - -If the procedure call has parameters, the must contain a sub-item. The sub-item can contain any number of s, each of which has a . - -Scalar s - -s can be scalars, type is indicated by nesting the value inside one of the tags listed in this table: - -Tag Type Example - or four-byte signed integer -12 - 0 (false) or 1 (true) 1 - string hello world - double-precision signed floating point number -12.214 - date/time 19980717T14:08:55 - base64-encoded binary eW91IGNhbid0IHJlYWQgdGhpcyE= - -If no type is indicated, the type is string. - -s - -A value can also be of type . - -A contains s and each contains a and a . - -Here's an example of a two-element : - - - - lowerBound - 18 - - - upperBound - 139 - - - -s can be recursive, any may contain a or any other type, including an , described below. - -s - -A value can also be of type . - -An contains a single element, which can contain any number of s. - -Here's an example of a four-element array: - - - - 12 - Egypt - 0 - -31 - - - - elements do not have names. - -You can mix types as the example above illustrates. - -s can be recursive, any value may contain an or any other type, including a , described above. - -Response example - -Here's an example of a response to an XML-RPC request: - -HTTP/1.1 200 OK -Connection: close -Content-Length: 158 -Content-Type: text/xml -Date: Fri, 17 Jul 1998 19:55:08 GMT -Server: UserLand Frontier/5.1.2-WinNT - - South Dakota - -Response format - -Unless there's a lower-level error, always return 200 OK. - -The Content-Type is text/xml. Content-Length must be present and correct. - -The body of the response is a single XML structure, a , which can contain a single which contains a single which contains a single . - -The could also contain a which contains a which is a containing two elements, one named , an and one named , a . - -A can not contain both a and a . - -Fault example - -HTTP/1.1 200 OK -Connection: close -Content-Length: 426 -Content-Type: text/xml -Date: Fri, 17 Jul 1998 19:55:02 GMT -Server: UserLand Frontier/5.1.2-WinNT - - faultCode 4 faultString Too many parameters. - -Strategies/Goals - -Firewalls. The goal of this protocol is to lay a compatible foundation across different environments, no new power is provided beyond the capabilities of the CGI interface. Firewall software can watch for POSTs whose Content-Type is text/xml. - -Discoverability. We wanted a clean, extensible format that's very simple. It should be possible for an HTML coder to be able to look at a file containing an XML-RPC procedure call, understand what it's doing, and be able to modify it and have it work on the first or second try. - -Easy to implement. We also wanted it to be an easy to implement protocol that could quickly be adapted to run in other environments or on other operating systems. - -Updated 1/21/99 DW - -The following questions came up on the UserLand discussion group as XML-RPC was being implemented in Python. - - The Response Format section says "The body of the response is a single XML structure, a , which can contain a single ..." This is confusing. Can we leave out the ? - - No you cannot leave it out if the procedure executed successfully. There are only two options, either a response contains a structure or it contains a structure. That's why we used the word "can" in that sentence. - - Is "boolean" a distinct data type, or can boolean values be interchanged with integers (e.g. zero=false, non-zero=true)? - - Yes, boolean is a distinct data type. Some languages/environments allow for an easy coercion from zero to false and one to true, but if you mean true, send a boolean type with the value true, so your intent can't possibly be misunderstood. - - What is the legal syntax (and range) for integers? How to deal with leading zeros? Is a leading plus sign allowed? How to deal with whitespace? - - An integer is a 32-bit signed number. You can include a plus or minus at the beginning of a string of numeric characters. Leading zeros are collapsed. Whitespace is not permitted. Just numeric characters preceeded by a plus or minus. - - What is the legal syntax (and range) for floating point values (doubles)? How is the exponent represented? How to deal with whitespace? Can infinity and "not a number" be represented? - - There is no representation for infinity or negative infinity or "not a number". At this time, only decimal point notation is allowed, a plus or a minus, followed by any number of numeric characters, followed by a period and any number of numeric characters. Whitespace is not allowed. The range of allowable values is implementation-dependent, is not specified. - - What characters are allowed in strings? Non-printable characters? Null characters? Can a "string" be used to hold an arbitrary chunk of binary data? - - Any characters are allowed in a string except < and &, which are encoded as < and &. A string can be used to encode binary data. - - Does the "struct" element keep the order of keys. Or in other words, is the struct "foo=1, bar=2" equivalent to "bar=2, foo=1" or not? - - The struct element does not preserve the order of the keys. The two structs are equivalent. - - Can the struct contain other members than and ? Is there a global list of faultCodes? (so they can be mapped to distinct exceptions for languages like Python and Java)? - - A struct may not contain members other than those specified. This is true for all other structures. We believe the specification is flexible enough so that all reasonable data-transfer needs can be accomodated within the specified structures. If you believe strongly that this is not true, please post a message on the discussion group. - - There is no global list of fault codes. It is up to the server implementer, or higher-level standards to specify fault codes. - - What timezone should be assumed for the dateTime.iso8601 type? UTC? localtime? - - Don't assume a timezone. It should be specified by the server in its documentation what assumptions it makes about timezones. - -Additions - - type. 1/21/99 DW. - -Updated 6/30/03 DW - -Removed "ASCII" from definition of string. - -Changed copyright dates, below, to 1999-2003 from 1998-99. - -Copyright and disclaimer - -© Copyright 1998-2003 UserLand Software. All Rights Reserved. - -This document and translations of it may be copied and furnished to others, and derivative works that comment on or otherwise explain it or assist in its implementation may be prepared, copied, published and distributed, in whole or in part, without restriction of any kind, provided that the above copyright notice and these paragraphs are included on all such copies and derivative works. - -This document may not be modified in any way, such as by removing the copyright notice or references to UserLand or other organizations. Further, while these copyright restrictions apply to the written XML-RPC specification, no claim of ownership is made by UserLand to the protocol it describes. Any party may, for commercial or non-commercial purposes, implement this protocol without royalty or license fee to UserLand. The limited permissions granted herein are perpetual and will not be revoked by UserLand or its successors or assigns. - -This document and the information contained herein is provided on an "AS IS" basis and USERLAND DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. diff --git a/OpenSubtitlesHandler/XML-RPC/Enums/XmlRpcValueType.cs b/OpenSubtitlesHandler/XML-RPC/Enums/XmlRpcValueType.cs deleted file mode 100644 index 51e0ee98f8..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Enums/XmlRpcValueType.cs +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -namespace XmlRpcHandler -{ - public enum XmlRpcBasicValueType - { - String, Int, Boolean, Double, dateTime_iso8601, base64 - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Types/XmlRpcMethodCall.cs b/OpenSubtitlesHandler/XML-RPC/Types/XmlRpcMethodCall.cs deleted file mode 100644 index 7cc1a164f6..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Types/XmlRpcMethodCall.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace XmlRpcHandler -{ - /// - /// A method call - /// - public struct XmlRpcMethodCall - { - /// - /// A method call - /// - /// The name of this method - public XmlRpcMethodCall(string name) - { - this.name = name; - this.parameters = new List(); - } - /// - /// A method call - /// - /// The name of this method - /// A list of parameters - public XmlRpcMethodCall(string name, List parameters) - { - this.name = name; - this.parameters = parameters; - } - - private string name; - private List parameters; - - /// - /// Get or set the name of this method - /// - public string Name - { get { return name; } set { name = value; } } - /// - /// Get or set the parameters to be sent - /// - public List Parameters - { get { return parameters; } set { parameters = value; } } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Values/IXmlRpcValue.cs b/OpenSubtitlesHandler/XML-RPC/Values/IXmlRpcValue.cs deleted file mode 100644 index c918790cba..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Values/IXmlRpcValue.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace XmlRpcHandler -{ - public abstract class IXmlRpcValue - { - public IXmlRpcValue() - { } - public IXmlRpcValue(object data) - { - this.data = data; - } - private object data; - /// - /// Get or set the data of this value - /// - public virtual object Data { get { return data; } set { data = value; } } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcStructMember.cs b/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcStructMember.cs deleted file mode 100644 index 2aad7ebffe..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcStructMember.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -namespace XmlRpcHandler -{ - public class XmlRpcStructMember - { - public XmlRpcStructMember(string name, IXmlRpcValue data) - { - this.name = name; - this.data = data; - } - private string name; - private IXmlRpcValue data; - - /// - /// Get or set the name of this member - /// - public string Name - { get { return name; } set { name = value; } } - /// - /// Get or set the data of this member - /// - public IXmlRpcValue Data - { get { return data; } set { data = value; } } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueArray.cs b/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueArray.cs deleted file mode 100644 index d10a80175b..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueArray.cs +++ /dev/null @@ -1,121 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; -using System.Collections.Generic; - -namespace XmlRpcHandler -{ - public class XmlRpcValueArray : IXmlRpcValue - { - public XmlRpcValueArray() : - base() - { - values = new List(); - } - public XmlRpcValueArray(object data) : - base(data) - { - values = new List(); - } - public XmlRpcValueArray(string[] texts) : - base() - { - values = new List(); - foreach (string val in texts) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(int[] ints) : - base() - { - values = new List(); - foreach (int val in ints) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(double[] doubles) : - base() - { - values = new List(); - foreach (double val in doubles) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(bool[] bools) : - base() - { - values = new List(); - foreach (bool val in bools) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(long[] base24s) : - base() - { - values = new List(); - foreach (long val in base24s) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(DateTime[] dates) : - base() - { - values = new List(); - foreach (var val in dates) - { - values.Add(new XmlRpcValueBasic(val)); - } - } - public XmlRpcValueArray(XmlRpcValueBasic[] basicValues) : - base() - { - values = new List(); - foreach (var val in basicValues) - { - values.Add(val); - } - } - public XmlRpcValueArray(XmlRpcValueStruct[] structs) : - base() - { - values = new List(); - foreach (var val in structs) - { - values.Add(val); - } - } - public XmlRpcValueArray(XmlRpcValueArray[] arrays) : - base() - { - values = new List(); - foreach (var val in arrays) - { - values.Add(val); - } - } - private List values; - - public List Values { get { return values; } set { values = value; } } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueBasic.cs b/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueBasic.cs deleted file mode 100644 index f2811b9889..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueBasic.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; - -namespace XmlRpcHandler -{ - public class XmlRpcValueBasic : IXmlRpcValue - { - public XmlRpcValueBasic() - : base() - { } - public XmlRpcValueBasic(string data) - : base(data) - { this.type = XmlRpcBasicValueType.String; } - public XmlRpcValueBasic(int data) - : base(data) - { this.type = XmlRpcBasicValueType.Int; } - public XmlRpcValueBasic(double data) - : base(data) - { this.type = XmlRpcBasicValueType.Double; } - public XmlRpcValueBasic(DateTime data) - : base(data) - { this.type = XmlRpcBasicValueType.dateTime_iso8601; } - public XmlRpcValueBasic(bool data) - : base(data) - { this.type = XmlRpcBasicValueType.Boolean; } - public XmlRpcValueBasic(long data) - : base(data) - { this.type = XmlRpcBasicValueType.base64; } - public XmlRpcValueBasic(object data, XmlRpcBasicValueType type) - : base(data) - { this.type = type; } - - private XmlRpcBasicValueType type = XmlRpcBasicValueType.String; - /// - /// Get or set the type of this basic value - /// - public XmlRpcBasicValueType ValueType { get { return type; } set { type = value; } } - /*Oprators. help a lot.*/ - public static implicit operator string(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return f.Data.ToString(); - else - throw new Exception("Unable to convert, this value is not string type."); - } - public static implicit operator int(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return (int)f.Data; - else - throw new Exception("Unable to convert, this value is not int type."); - } - public static implicit operator double(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return (double)f.Data; - else - throw new Exception("Unable to convert, this value is not double type."); - } - public static implicit operator bool(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return (bool)f.Data; - else - throw new Exception("Unable to convert, this value is not bool type."); - } - public static implicit operator long(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return (long)f.Data; - else - throw new Exception("Unable to convert, this value is not long type."); - } - public static implicit operator DateTime(XmlRpcValueBasic f) - { - if (f.type == XmlRpcBasicValueType.String) - return (DateTime)f.Data; - else - throw new Exception("Unable to convert, this value is not DateTime type."); - } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueStruct.cs b/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueStruct.cs deleted file mode 100644 index 4863e38e84..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/Values/XmlRpcValueStruct.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -using System.Collections.Generic; - -namespace XmlRpcHandler -{ - /// - /// Represents XML-RPC struct - /// - public class XmlRpcValueStruct : IXmlRpcValue - { - /// - /// Represents XML-RPC struct - /// - /// - public XmlRpcValueStruct(List members) - { this.members = members; } - - private List members; - /// - /// Get or set the members collection - /// - public List Members - { get { return members; } set { members = value; } } - } -} diff --git a/OpenSubtitlesHandler/XML-RPC/XmlRpcGenerator.cs b/OpenSubtitlesHandler/XML-RPC/XmlRpcGenerator.cs deleted file mode 100644 index a79a278fa1..0000000000 --- a/OpenSubtitlesHandler/XML-RPC/XmlRpcGenerator.cs +++ /dev/null @@ -1,350 +0,0 @@ -/* This file is part of OpenSubtitles Handler - A library that handle OpenSubtitles.org XML-RPC methods. - - Copyright © Ala Ibrahim Hadid 2013 - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using OpenSubtitlesHandler; - -namespace XmlRpcHandler -{ - /// - /// A class for making XML-RPC requests. The requests then can be sent directly as http request. - /// - public class XmlRpcGenerator - { - /// - /// Generate XML-RPC request using method call. - /// - /// The method call - /// The request in xml. - public static byte[] Generate(XmlRpcMethodCall method) - { return Generate(new XmlRpcMethodCall[] { method }); } - /// - /// Generate XML-RPC request using method calls array. - /// - /// The method calls array - /// The request in utf8 xml string as buffer - public static byte[] Generate(XmlRpcMethodCall[] methods) - { - if (methods == null) - throw new Exception("No method to write !"); - if (methods.Length == 0) - throw new Exception("No method to write !"); - // Create xml - var sett = new XmlWriterSettings(); - sett.Indent = true; - - sett.Encoding = Encoding.UTF8; - - using (var ms = new MemoryStream()) - { - using (var XMLwrt = XmlWriter.Create(ms, sett)) - { - // Let's write the methods - foreach (XmlRpcMethodCall method in methods) - { - XMLwrt.WriteStartElement("methodCall");//methodCall - XMLwrt.WriteStartElement("methodName");//methodName - XMLwrt.WriteString(method.Name); - XMLwrt.WriteEndElement();//methodName - XMLwrt.WriteStartElement("params");//params - // Write values - foreach (IXmlRpcValue p in method.Parameters) - { - XMLwrt.WriteStartElement("param");//param - if (p is XmlRpcValueBasic) - { - WriteBasicValue(XMLwrt, (XmlRpcValueBasic)p); - } - else if (p is XmlRpcValueStruct) - { - WriteStructValue(XMLwrt, (XmlRpcValueStruct)p); - } - else if (p is XmlRpcValueArray) - { - WriteArrayValue(XMLwrt, (XmlRpcValueArray)p); - } - XMLwrt.WriteEndElement();//param - } - - XMLwrt.WriteEndElement();//params - XMLwrt.WriteEndElement();//methodCall - } - XMLwrt.Flush(); - return ms.ToArray(); - } - } - } - /// - /// Decode response then return the values - /// - /// The response xml string as provided by server as methodResponse - /// - public static XmlRpcMethodCall[] DecodeMethodResponse(string xmlResponse) - { - var methods = new List(); - var sett = new XmlReaderSettings(); - sett.DtdProcessing = DtdProcessing.Ignore; - sett.IgnoreWhitespace = true; - MemoryStream str; - if (xmlResponse.Contains(@"encoding=""utf-8""")) - { - str = new MemoryStream(Encoding.UTF8.GetBytes(xmlResponse)); - } - else - { - str = new MemoryStream(Utilities.GetASCIIBytes(xmlResponse)); - } - using (str) - { - using (var XMLread = XmlReader.Create(str, sett)) - { - var call = new XmlRpcMethodCall("methodResponse"); - // Read parameters - while (XMLread.Read()) - { - if (XMLread.Name == "param" && XMLread.IsStartElement()) - { - IXmlRpcValue val = ReadValue(XMLread); - if (val != null) - call.Parameters.Add(val); - } - } - methods.Add(call); - return methods.ToArray(); - } - } - } - - private static void WriteBasicValue(XmlWriter XMLwrt, XmlRpcValueBasic val) - { - XMLwrt.WriteStartElement("value");//value - switch (val.ValueType) - { - case XmlRpcBasicValueType.String: - XMLwrt.WriteStartElement("string"); - if (val.Data != null) - XMLwrt.WriteString(val.Data.ToString()); - XMLwrt.WriteEndElement(); - break; - case XmlRpcBasicValueType.Int: - XMLwrt.WriteStartElement("int"); - if (val.Data != null) - XMLwrt.WriteString(val.Data.ToString()); - XMLwrt.WriteEndElement(); - break; - case XmlRpcBasicValueType.Boolean: - XMLwrt.WriteStartElement("boolean"); - if (val.Data != null) - XMLwrt.WriteString(((bool)val.Data) ? "1" : "0"); - XMLwrt.WriteEndElement(); - break; - case XmlRpcBasicValueType.Double: - XMLwrt.WriteStartElement("double"); - if (val.Data != null) - XMLwrt.WriteString(val.Data.ToString()); - XMLwrt.WriteEndElement(); - break; - case XmlRpcBasicValueType.dateTime_iso8601: - XMLwrt.WriteStartElement("dateTime.iso8601"); - // Get date time format - if (val.Data != null) - { - var time = (DateTime)val.Data; - string dt = time.Year + time.Month.ToString("D2") + time.Day.ToString("D2") + - "T" + time.Hour.ToString("D2") + ":" + time.Minute.ToString("D2") + ":" + - time.Second.ToString("D2"); - XMLwrt.WriteString(dt); - } - XMLwrt.WriteEndElement(); - break; - case XmlRpcBasicValueType.base64: - XMLwrt.WriteStartElement("base64"); - if (val.Data != null) - XMLwrt.WriteString(Convert.ToBase64String(BitConverter.GetBytes((long)val.Data))); - XMLwrt.WriteEndElement(); - break; - } - XMLwrt.WriteEndElement();//value - } - private static void WriteStructValue(XmlWriter XMLwrt, XmlRpcValueStruct val) - { - XMLwrt.WriteStartElement("value");//value - XMLwrt.WriteStartElement("struct");//struct - foreach (XmlRpcStructMember member in val.Members) - { - XMLwrt.WriteStartElement("member");//member - - XMLwrt.WriteStartElement("name");//name - XMLwrt.WriteString(member.Name); - XMLwrt.WriteEndElement();//name - - if (member.Data is XmlRpcValueBasic) - { - WriteBasicValue(XMLwrt, (XmlRpcValueBasic)member.Data); - } - else if (member.Data is XmlRpcValueStruct) - { - WriteStructValue(XMLwrt, (XmlRpcValueStruct)member.Data); - } - else if (member.Data is XmlRpcValueArray) - { - WriteArrayValue(XMLwrt, (XmlRpcValueArray)member.Data); - } - - XMLwrt.WriteEndElement();//member - } - XMLwrt.WriteEndElement();//struct - XMLwrt.WriteEndElement();//value - } - private static void WriteArrayValue(XmlWriter XMLwrt, XmlRpcValueArray val) - { - XMLwrt.WriteStartElement("value");//value - XMLwrt.WriteStartElement("array");//array - XMLwrt.WriteStartElement("data");//data - foreach (IXmlRpcValue o in val.Values) - { - if (o is XmlRpcValueBasic) - { - WriteBasicValue(XMLwrt, (XmlRpcValueBasic)o); - } - else if (o is XmlRpcValueStruct) - { - WriteStructValue(XMLwrt, (XmlRpcValueStruct)o); - } - else if (o is XmlRpcValueArray) - { - WriteArrayValue(XMLwrt, (XmlRpcValueArray)o); - } - } - XMLwrt.WriteEndElement();//data - XMLwrt.WriteEndElement();//array - XMLwrt.WriteEndElement();//value - } - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private static string ReadString(XmlReader reader) - { - if (reader.NodeType == XmlNodeType.Element) - { - return reader.ReadElementContentAsString(); - } - return reader.ReadContentAsString(); - } - - private static IXmlRpcValue ReadValue(XmlReader xmlReader, bool skipRead = false) - { - while (skipRead || xmlReader.Read()) - { - if (xmlReader.Name == "value" && xmlReader.IsStartElement()) - { - xmlReader.Read(); - if (xmlReader.Name == "string" && xmlReader.IsStartElement()) - { - return new XmlRpcValueBasic(ReadString(xmlReader), XmlRpcBasicValueType.String); - } - else if (xmlReader.Name == "int" && xmlReader.IsStartElement()) - { - return new XmlRpcValueBasic(int.Parse(ReadString(xmlReader), UsCulture), XmlRpcBasicValueType.Int); - } - else if (xmlReader.Name == "boolean" && xmlReader.IsStartElement()) - { - return new XmlRpcValueBasic(ReadString(xmlReader) == "1", XmlRpcBasicValueType.Boolean); - } - else if (xmlReader.Name == "double" && xmlReader.IsStartElement()) - { - return new XmlRpcValueBasic(double.Parse(ReadString(xmlReader), UsCulture), XmlRpcBasicValueType.Double); - } - else if (xmlReader.Name == "dateTime.iso8601" && xmlReader.IsStartElement()) - { - string date = ReadString(xmlReader); - int year = int.Parse(date.Substring(0, 4), UsCulture); - int month = int.Parse(date.Substring(4, 2), UsCulture); - int day = int.Parse(date.Substring(6, 2), UsCulture); - int hour = int.Parse(date.Substring(9, 2), UsCulture); - int minute = int.Parse(date.Substring(12, 2), UsCulture);//19980717T14:08:55 - int sec = int.Parse(date.Substring(15, 2), UsCulture); - var time = new DateTime(year, month, day, hour, minute, sec); - return new XmlRpcValueBasic(time, XmlRpcBasicValueType.dateTime_iso8601); - } - else if (xmlReader.Name == "base64" && xmlReader.IsStartElement()) - { - return new XmlRpcValueBasic(BitConverter.ToInt64(Convert.FromBase64String(ReadString(xmlReader)), 0) - , XmlRpcBasicValueType.Double); - } - else if (xmlReader.Name == "struct" && xmlReader.IsStartElement()) - { - var strct = new XmlRpcValueStruct(new List()); - // Read members... - while (xmlReader.Read()) - { - if (xmlReader.Name == "member" && xmlReader.IsStartElement()) - { - var member = new XmlRpcStructMember("", null); - xmlReader.Read();// read name - member.Name = ReadString(xmlReader); - - IXmlRpcValue val = ReadValue(xmlReader, true); - if (val != null) - { - member.Data = val; - strct.Members.Add(member); - } - } - else if (xmlReader.Name == "struct" && !xmlReader.IsStartElement()) - { - return strct; - } - } - return strct; - } - else if (xmlReader.Name == "array" && xmlReader.IsStartElement()) - { - var array = new XmlRpcValueArray(); - // Read members... - while (xmlReader.Read()) - { - if (xmlReader.Name == "array" && !xmlReader.IsStartElement()) - { - return array; - } - else - { - IXmlRpcValue val = ReadValue(xmlReader); - if (val != null) - array.Values.Add(val); - } - } - return array; - } - } - else break; - - if (skipRead) - { - return null; - } - } - return null; - } - } -} From 3c4043199accbfe7995dd6060c89fc837300884a Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 12 Mar 2019 09:18:45 -0400 Subject: [PATCH 0209/6208] Implement review feedback --- Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 4 ++-- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/StartupOptions.cs | 2 +- MediaBrowser.Common/Configuration/IApplicationPaths.cs | 4 ++-- MediaBrowser.Model/System/SystemInfo.cs | 4 ++-- MediaBrowser.WebDashboard/Api/DashboardService.cs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index fd9ce3a364..00cfa0c9a9 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -36,9 +36,9 @@ namespace Emby.Server.Implementations.AppBase public string ProgramDataPath { get; private set; } /// - /// Gets the path to the web resources folder + /// Gets the path to the web UI resources folder /// - /// The web resources path. + /// The web UI resources path. public string WebPath { get; set; } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f240ef871a..14c700cc5d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -621,7 +621,7 @@ namespace Emby.Server.Implementations string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; if (string.IsNullOrEmpty(contentRoot)) { - contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.WebPath, "src"); + contentRoot = ServerConfigurationManager.ApplicationPaths.WebPath; } var host = new WebHostBuilder() diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8f1498b13b..7534a6398f 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -277,7 +277,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(webDir)) { // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web", "src"); } } diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 2b01c7bbdf..345a8a731f 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Server [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] public string DataDir { get; set; } - [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web resources.")] + [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web UI resources.")] public string WebDir { get; set; } [Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")] diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 1f56c98fd8..fd11bf904f 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -12,9 +12,9 @@ namespace MediaBrowser.Common.Configuration string ProgramDataPath { get; } /// - /// Gets the path to the web resources folder + /// Gets the path to the web UI resources folder /// - /// The web resources path. + /// The web UI resources path. string WebPath { get; } /// diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index fa72624819..222c10798a 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -84,9 +84,9 @@ namespace MediaBrowser.Model.System public string ProgramDataPath { get; set; } /// - /// Gets or sets the web resources path. + /// Gets or sets the web UI resources path. /// - /// The web resources path. + /// The web UI resources path. public string WebPath { get; set; } /// diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 05df54b115..01cbe2c314 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -149,7 +149,7 @@ namespace MediaBrowser.WebDashboard.Api return _serverConfigurationManager.Configuration.DashboardSourcePath; } - return Path.Combine(_serverConfigurationManager.ApplicationPaths.WebPath, "src"); + return _serverConfigurationManager.ApplicationPaths.WebPath; } } From afdef163ea53db1aa740cf4051322ff2fe190022 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 12 Mar 2019 20:49:29 +0100 Subject: [PATCH 0210/6208] Fix build by removing non existent namespace Looks like a wrong auto merge. (We really should fix CI) --- MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 3eb32fe16b..aee9921034 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Xml; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; From b864e9da2a512250eff48baae28251db5a508f8f Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Tue, 12 Mar 2019 22:09:18 +0000 Subject: [PATCH 0211/6208] Finalise removal of --ffprobe switch Removed --ffprobe from src files and server/docker scripts. --- Dockerfile | 3 +-- Dockerfile.arm | 3 +-- Dockerfile.arm64 | 3 +-- Emby.Server.Implementations/ApplicationHost.cs | 1 - Emby.Server.Implementations/IStartupOptions.cs | 5 ----- Jellyfin.Server/StartupOptions.cs | 3 --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 9 --------- deployment/debian-package-x64/pkg-src/conf/jellyfin | 3 +-- deployment/debian-package-x64/pkg-src/jellyfin.service | 2 +- deployment/fedora-package-x64/pkg-src/jellyfin.env | 1 - deployment/fedora-package-x64/pkg-src/jellyfin.service | 2 +- 11 files changed, 6 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91a4f5a2d2..72c0cdbf3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,5 +31,4 @@ VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ --datadir /config \ --cachedir /cache \ - --ffmpeg /usr/local/bin/ffmpeg \ - --ffprobe /usr/local/bin/ffprobe + --ffmpeg /usr/local/bin/ffmpeg diff --git a/Dockerfile.arm b/Dockerfile.arm index 42f0354a32..238e90ccd8 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -39,5 +39,4 @@ VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ --datadir /config \ --cachedir /cache \ - --ffmpeg /usr/bin/ffmpeg \ - --ffprobe /usr/bin/ffprobe + --ffmpeg /usr/bin/ffmpeg diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index d3103d3893..2927d4e658 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -40,5 +40,4 @@ VOLUME /cache /config /media ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ --datadir /config \ --cachedir /cache \ - --ffmpeg /usr/bin/ffmpeg \ - --ffprobe /usr/bin/ffprobe + --ffmpeg /usr/bin/ffmpeg diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 98f6ab6bc7..1810064e69 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -837,7 +837,6 @@ namespace Emby.Server.Implementations LoggerFactory, JsonSerializer, StartupOptions.FFmpegPath, - StartupOptions.FFprobePath, ServerConfigurationManager, FileSystemManager, () => SubtitleEncoder, diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 24aaa76c02..6e915de3d1 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -7,11 +7,6 @@ namespace Emby.Server.Implementations /// string FFmpegPath { get; } - /// - /// --ffprobe - /// - string FFprobePath { get; } - /// /// --service /// diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 345a8a731f..8296d414ef 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -26,9 +26,6 @@ namespace Jellyfin.Server [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } - [Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")] - public string FFprobePath { get; set; } - [Option("service", Required = false, HelpText = "Run as headless service.")] public bool IsService { get; set; } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 94beda3dbb..f454c27235 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -51,7 +51,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IProcessFactory _processFactory; private readonly int DefaultImageExtractionTimeoutMs; private readonly string StartupOptionFFmpegPath; - private readonly string StartupOptionFFprobePath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly List _runningProcesses = new List(); @@ -60,7 +59,6 @@ namespace MediaBrowser.MediaEncoding.Encoder ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, string startupOptionsFFmpegPath, - string startupOptionsFFprobePath, IServerConfigurationManager configurationManager, IFileSystem fileSystem, Func subtitleEncoder, @@ -71,7 +69,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _jsonSerializer = jsonSerializer; StartupOptionFFmpegPath = startupOptionsFFmpegPath; - StartupOptionFFprobePath = startupOptionsFFprobePath; ConfigurationManager = configurationManager; FileSystem = fileSystem; SubtitleEncoder = subtitleEncoder; @@ -86,12 +83,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public void SetFFmpegPath() { - // ToDo - Finalise removal of the --ffprobe switch - if (!string.IsNullOrEmpty(StartupOptionFFprobePath)) - { - _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release"); - } - // 1) Custom path stored in config/encoding xml file under tag takes precedence if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index 7db482d428..bc00c37e20 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -23,7 +23,6 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" # ffmpeg binary paths, overriding the system values JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg" -JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" @@ -38,4 +37,4 @@ JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" +JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.service b/deployment/debian-package-x64/pkg-src/jellyfin.service index b4da3a945b..1305e238b0 100644 --- a/deployment/debian-package-x64/pkg-src/jellyfin.service +++ b/deployment/debian-package-x64/pkg-src/jellyfin.service @@ -6,7 +6,7 @@ After = network.target Type = simple EnvironmentFile = /etc/default/jellyfin User = jellyfin -ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} Restart = on-failure TimeoutSec = 15 diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.env b/deployment/fedora-package-x64/pkg-src/jellyfin.env index 143a317c43..de48f13af5 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.env +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.env @@ -25,7 +25,6 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" # [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values #JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg" -#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe" # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.service b/deployment/fedora-package-x64/pkg-src/jellyfin.service index 1f83d3d386..f3dc594b1c 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.service +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.service @@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of [Service] EnvironmentFile=/etc/sysconfig/jellyfin WorkingDirectory=/var/lib/jellyfin -ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} TimeoutSec=15 Restart=on-failure User=jellyfin From 4cc4d57a782441f02f8901af29634ffbfd4a2fc2 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Wed, 13 Mar 2019 20:37:08 +0100 Subject: [PATCH 0212/6208] Disable the Drone CI ABI check --- .drone.yml | 81 ------------------------------------------------------ 1 file changed, 81 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7705f4f936..87c8e414e9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -28,84 +28,3 @@ steps: commands: - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release" ---- - -kind: pipeline -name: check-abi - -steps: -- name: submodules - image: docker:git - commands: - - git submodule update --init --recursive - -- name: build - image: microsoft/dotnet:2-sdk - commands: - - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release" - -- name: clone-dotnet-compat - image: docker:git - commands: - - git clone --depth 1 https://github.com/EraYaN/dotnet-compatibility ci/dotnet-compatibility - -- name: build-dotnet-compat - image: microsoft/dotnet:2-sdk - commands: - - dotnet publish "ci/dotnet-compatibility/CompatibilityCheckerCoreCLI" --configuration Release --output "../../ci-tools" - -- name: download-last-nuget-release-common - image: plugins/download - settings: - source: https://www.nuget.org/api/v2/package/Jellyfin.Common - destination: ci/Jellyfin.Common.nupkg - -- name: download-last-nuget-release-model - image: plugins/download - settings: - source: https://www.nuget.org/api/v2/package/Jellyfin.Model - destination: ci/Jellyfin.Model.nupkg - -- name: download-last-nuget-release-controller - image: plugins/download - settings: - source: https://www.nuget.org/api/v2/package/Jellyfin.Controller - destination: ci/Jellyfin.Controller.nupkg - -- name: download-last-nuget-release-naming - image: plugins/download - settings: - source: https://www.nuget.org/api/v2/package/Jellyfin.Naming - destination: ci/Jellyfin.Naming.nupkg - -- name: extract-downloaded-nuget-packages - image: garthk/unzip - commands: - - unzip -j ci/Jellyfin.Common.nupkg "*.dll" -d ci/nuget-packages - - unzip -j ci/Jellyfin.Model.nupkg "*.dll" -d ci/nuget-packages - - unzip -j ci/Jellyfin.Controller.nupkg "*.dll" -d ci/nuget-packages - - unzip -j ci/Jellyfin.Naming.nupkg "*.dll" -d ci/nuget-packages - -- name: run-dotnet-compat-common - image: microsoft/dotnet:2-runtime - err_ignore: true - commands: - - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Common.dll ci/ci-release/MediaBrowser.Common.dll - -- name: run-dotnet-compat-model - image: microsoft/dotnet:2-runtime - err_ignore: true - commands: - - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Model.dll ci/ci-release/MediaBrowser.Model.dll - -- name: run-dotnet-compat-controller - image: microsoft/dotnet:2-runtime - err_ignore: true - commands: - - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Controller.dll ci/ci-release/MediaBrowser.Controller.dll - -- name: run-dotnet-compat-naming - image: microsoft/dotnet:2-runtime - err_ignore: true - commands: - - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/Emby.Naming.dll ci/ci-release/Emby.Naming.dll From 1d443d2ff5ef9edaf7040633ec737d043afeafa6 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Wed, 13 Mar 2019 21:09:08 +0100 Subject: [PATCH 0213/6208] Do not use the nuget packages for comparison, but the last master build (#1091) * Do not use the nuget packages for comparison, but the last master build. * Only allow passing builds. --- .ci/azure-pipelines.yml | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 4cd19dc838..7a92d40889 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -11,9 +11,6 @@ pr: trigger: batch: true - branches: - include: - - master jobs: - job: main_build @@ -71,28 +68,28 @@ jobs: - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Naming' - condition: eq(variables['BuildConfiguration'], 'Release') + condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) inputs: PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll' artifactName: 'Jellyfin.Naming' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Controller' - condition: eq(variables['BuildConfiguration'], 'Release') + condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) inputs: PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' artifactName: 'Jellyfin.Controller' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Model' - condition: eq(variables['BuildConfiguration'], 'Release') + condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) inputs: PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll' artifactName: 'Jellyfin.Model' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact Common' - condition: eq(variables['BuildConfiguration'], 'Release') + condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) inputs: PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll' artifactName: 'Jellyfin.Common' @@ -102,7 +99,7 @@ jobs: pool: vmImage: ubuntu-16.04 dependsOn: main_build - condition: succeeded() + condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) strategy: matrix: Naming: @@ -121,16 +118,28 @@ jobs: steps: - checkout: none - - task: NuGetCommand@2 - displayName: 'Download $(NugetPackageName)' + - task: DownloadBuildArtifacts@0 + displayName: Download the Reference Assembly Build Artifact inputs: - command: custom - arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload' + buildType: 'specific' # Options: current, specific + project: $(System.TeamProjectId) # Required when buildType == Specific + pipeline: $(System.DefinitionId) # Required when buildType == Specific, not sure if this will take a name too + #specificBuildWithTriggering: false # Optional + buildVersionToDownload: 'latestFromBranch' # Required when buildType == Specific# Options: latest, latestFromBranch, specific + allowPartiallySucceededBuilds: false # Optional + branchName: '$(System.PullRequest.TargetBranch)' # Required when buildType == Specific && BuildVersionToDownload == LatestFromBranch + #buildId: # Required when buildType == Specific && BuildVersionToDownload == Specific + #tags: # Optional + downloadType: 'single' # Options: single, specific + artifactName: '$(NugetPackageName)'# Required when downloadType == Single + #itemPattern: '**' # Optional + downloadPath: '$(System.ArtifactsDirectory)/current-artifacts' + #parallelizationLimit: '8' # Optional - task: CopyFiles@2 displayName: Copy Nuget Assembly to current-release folder inputs: - sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional + sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional contents: '**/*.dll' targetFolder: $(System.ArtifactsDirectory)/current-release cleanTargetFolder: true # Optional @@ -138,7 +147,7 @@ jobs: flattenFolders: true # Optional - task: DownloadBuildArtifacts@0 - displayName: Download the Assembly Build Artifact + displayName: Download the New Assembly Build Artifact inputs: buildType: 'current' # Options: current, specific allowPartiallySucceededBuilds: false # Optional From e64aaebbacfa7a720c99ca2ab1aa11f7fcd63868 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 13 Mar 2019 17:51:33 +0100 Subject: [PATCH 0214/6208] Improvements around streams * Use ArrayPool instead of allocating new buffers each time * Remove NetworkStream copy * Remove some dead code --- .../HttpServer/StreamWriter.cs | 1 - .../IO/StreamHelper.cs | 234 +++++++++++------- .../HdHomerun/HdHomerunUdpStream.cs | 3 +- .../LiveTv/TunerHosts/LiveStream.cs | 58 ++--- .../Net/DisposableManagedObjectBase.cs | 66 ----- .../Net/SocketFactory.cs | 134 +++------- Emby.Server.Implementations/Net/UdpSocket.cs | 74 +++--- MediaBrowser.Api/LiveTv/LiveTvService.cs | 32 ++- .../LiveTv/ProgressiveFileCopier.cs | 39 +-- MediaBrowser.Model/Net/HttpResponse.cs | 64 ----- MediaBrowser.Model/Net/IAcceptSocket.cs | 15 -- 11 files changed, 269 insertions(+), 451 deletions(-) delete mode 100644 Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs delete mode 100644 MediaBrowser.Model/Net/HttpResponse.cs delete mode 100644 MediaBrowser.Model/Net/IAcceptSocket.cs diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 66a13e20df..cf30bbc326 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -5,7 +5,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index 09cf4d4a3e..d02cd84a03 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -8,168 +9,213 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { + private const int StreamCopyToBufferSize = 81920; + public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) { - byte[] buffer = new byte[bufferSize]; - int read; - while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try { - cancellationToken.ThrowIfCancellationRequested(); - - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); - - if (onStarted != null) + int read; + while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) { - onStarted(); - onStarted = null; + cancellationToken.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + + if (onStarted != null) + { + onStarted(); + onStarted = null; + } } } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken) { - byte[] buffer = new byte[bufferSize]; - - if (emptyReadLimit <= 0) + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try { - int read; - while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + if (emptyReadLimit <= 0) + { + int read; + while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + } + + return; + } + + var eofCount = 0; + + while (eofCount < emptyReadLimit) { cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); - } + var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - return; + if (bytesRead == 0) + { + eofCount++; + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + else + { + eofCount = 0; + + await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + } + } } - - var eofCount = 0; - - while (eofCount < emptyReadLimit) + finally { - cancellationToken.ThrowIfCancellationRequested(); - - var bytesRead = source.Read(buffer, 0, buffer.Length); - - if (bytesRead == 0) - { - eofCount++; - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } - else - { - eofCount = 0; - - await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); - } + ArrayPool.Shared.Return(buffer); } } - const int StreamCopyToBufferSize = 81920; public async Task CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + try { - var bytesToWrite = bytesRead; + int totalBytesRead = 0; - if (bytesToWrite > 0) + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + var bytesToWrite = bytesRead; - totalBytesRead += bytesRead; + if (bytesToWrite > 0) + { + await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + totalBytesRead += bytesRead; + } } - } - return totalBytesRead; + return totalBytesRead; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + try { - var bytesToWrite = bytesRead; + int bytesRead; + int totalBytesRead = 0; - if (bytesToWrite > 0) + while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + var bytesToWrite = bytesRead; - totalBytesRead += bytesRead; + if (bytesToWrite > 0) + { + await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + totalBytesRead += bytesRead; + } } - } - return totalBytesRead; + return totalBytesRead; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - - while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + try { - var bytesToWrite = Math.Min(bytesRead, copyLength); + int bytesRead; - if (bytesToWrite > 0) + while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } + var bytesToWrite = Math.Min(bytesRead, copyLength); - copyLength -= bytesToWrite; + if (bytesToWrite > 0) + { + await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } - if (copyLength <= 0) - { - break; + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } } } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + try { - var bytesToWrite = Math.Min(bytesRead, copyLength); + int bytesRead; - if (bytesToWrite > 0) + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } + var bytesToWrite = Math.Min(bytesRead, copyLength); - copyLength -= bytesToWrite; + if (bytesToWrite > 0) + { + await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } - if (copyLength <= 0) - { - break; + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } } } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken) { - byte[] buffer = new byte[bufferSize]; - - while (!cancellationToken.IsCancellationRequested) + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try { - var bytesRead = await CopyToAsyncInternal(source, target, buffer, cancellationToken).ConfigureAwait(false); - - //var position = fs.Position; - //_logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); - - if (bytesRead == 0) + while (!cancellationToken.IsCancellationRequested) { - await Task.Delay(100).ConfigureAwait(false); + var bytesRead = await CopyToAsyncInternal(source, target, buffer, cancellationToken).ConfigureAwait(false); + + if (bytesRead == 0) + { + await Task.Delay(100).ConfigureAwait(false); + } } } + finally + { + ArrayPool.Shared.Return(buffer); + } } private static async Task CopyToAsyncInternal(Stream source, Stream destination, byte[] buffer, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 8774371d52..7f426ea31f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -151,7 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } - private static int RtpHeaderBytes = 12; + private const int RtpHeaderBytes = 12; + private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) { var bufferSize = 81920; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 1f8ca276e7..ece2cbd547 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public string OriginalStreamId { get; set; } public bool EnableStreamSharing { get; set; } - public string UniqueId { get; private set; } + public string UniqueId { get; } protected readonly IFileSystem FileSystem; protected readonly IServerApplicationPaths AppPaths; @@ -31,12 +31,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts protected readonly ILogger Logger; protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource(); - public string TunerHostId { get; private set; } + public string TunerHostId { get; } public DateTime DateOpened { get; protected set; } - public Func OnClose { get; set; } - public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) { OriginalMediaSource = mediaSource; @@ -76,26 +74,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts LiveStreamCancellationTokenSource.Cancel(); - if (OnClose != null) - { - return CloseWithExternalFn(); - } - return Task.CompletedTask; } - private async Task CloseWithExternalFn() - { - try - { - await OnClose().ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error closing live stream"); - } - } - protected Stream GetInputStream(string path, bool allowAsyncFileRead) { var fileOpenOptions = FileOpenOptions.SequentialScan; @@ -113,27 +94,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return DeleteTempFiles(GetStreamFilePaths()); } - protected async Task DeleteTempFiles(List paths, int retryCount = 0) + protected async Task DeleteTempFiles(IEnumerable paths, int retryCount = 0) { if (retryCount == 0) { - Logger.LogInformation("Deleting temp files {0}", string.Join(", ", paths.ToArray())); + Logger.LogInformation("Deleting temp files {0}", paths); } var failedFiles = new List(); foreach (var path in paths) { + if (!File.Exists(path)) + { + continue; + } + try { FileSystem.DeleteFile(path); } - catch (DirectoryNotFoundException) - { - } - catch (FileNotFoundException) - { - } catch (Exception ex) { Logger.LogError(ex, "Error deleting file {path}", path); @@ -157,8 +137,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token; - var allowAsync = false; - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039 + var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT; bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; @@ -181,28 +161,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.LogInformation("Live Stream ended."); } - private Tuple GetNextFile(string currentFile) + private (string file, bool isLastFile) GetNextFile(string currentFile) { var files = GetStreamFilePaths(); - //logger.LogInformation("Live stream files: {0}", string.Join(", ", files.ToArray())); - if (string.IsNullOrEmpty(currentFile)) { - return new Tuple(files.Last(), true); + return (files.Last(), true); } var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1; var isLastFile = nextIndex == files.Count - 1; - return new Tuple(files.ElementAtOrDefault(nextIndex), isLastFile); + return (files.ElementAtOrDefault(nextIndex), isLastFile); } private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken) { - //logger.LogInformation("Opening live stream file {0}. Empty read limit: {1}", path, emptyReadLimit); - using (var inputStream = (FileStream)GetInputStream(path, allowAsync)) { if (seekFile) @@ -218,7 +194,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private void TrySeek(FileStream stream, long offset) { - //logger.LogInformation("TrySeek live stream"); + if (!stream.CanSeek) + { + return; + } + try { stream.Seek(offset, SeekOrigin.End); diff --git a/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs b/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs deleted file mode 100644 index 304b445651..0000000000 --- a/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; - -namespace Emby.Server.Implementations.Net -{ - /// - /// Correclty implements the interface and pattern for an object containing only managed resources, and adds a few common niceities not on the interface such as an property. - /// - public abstract class DisposableManagedObjectBase : IDisposable - { - - #region Public Methods - - /// - /// Override this method and dispose any objects you own the lifetime of if disposing is true; - /// - /// True if managed objects should be disposed, if false, only unmanaged resources should be released. - protected abstract void Dispose(bool disposing); - - - //TODO Remove and reimplement using the IsDisposed property directly. - /// - /// Throws an if the property is true. - /// - /// - /// Thrown if the property is true. - /// - protected virtual void ThrowIfDisposed() - { - if (IsDisposed) throw new ObjectDisposedException(GetType().Name); - } - - #endregion - - #region Public Properties - - /// - /// Sets or returns a boolean indicating whether or not this instance has been disposed. - /// - /// - public bool IsDisposed - { - get; - private set; - } - - #endregion - - #region IDisposable Members - - /// - /// Disposes this object instance and all internally managed resources. - /// - /// - /// Sets the property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes. - /// - /// - public void Dispose() - { - IsDisposed = true; - - Dispose(true); - } - - #endregion - } -} diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 6beb14f558..492f48abe8 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -4,7 +4,6 @@ using System.Net; using System.Net.Sockets; using Emby.Server.Implementations.Networking; using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Net { @@ -19,7 +18,10 @@ namespace Emby.Server.Implementations.Net public ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort) { - if (remotePort < 0) throw new ArgumentException("remotePort cannot be less than zero.", nameof(remotePort)); + if (remotePort < 0) + { + throw new ArgumentException("remotePort cannot be less than zero.", nameof(remotePort)); + } var addressFamily = remoteAddress.AddressFamily == IpAddressFamily.InterNetwork ? AddressFamily.InterNetwork @@ -42,8 +44,7 @@ namespace Emby.Server.Implementations.Net } catch { - if (retVal != null) - retVal.Dispose(); + retVal?.Dispose(); throw; } @@ -55,7 +56,10 @@ namespace Emby.Server.Implementations.Net /// An integer specifying the local port to bind the acceptSocket to. public ISocket CreateUdpSocket(int localPort) { - if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + if (localPort < 0) + { + throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + } var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); try @@ -65,8 +69,7 @@ namespace Emby.Server.Implementations.Net } catch { - if (retVal != null) - retVal.Dispose(); + retVal?.Dispose(); throw; } @@ -74,7 +77,10 @@ namespace Emby.Server.Implementations.Net public ISocket CreateUdpBroadcastSocket(int localPort) { - if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + if (localPort < 0) + { + throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + } var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); try @@ -86,8 +92,7 @@ namespace Emby.Server.Implementations.Net } catch { - if (retVal != null) - retVal.Dispose(); + retVal?.Dispose(); throw; } @@ -99,7 +104,10 @@ namespace Emby.Server.Implementations.Net /// An implementation of the interface used by RSSDP components to perform acceptSocket operations. public ISocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort) { - if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + if (localPort < 0) + { + throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + } var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); try @@ -114,8 +122,7 @@ namespace Emby.Server.Implementations.Net } catch { - if (retVal != null) - retVal.Dispose(); + retVal?.Dispose(); throw; } @@ -130,10 +137,25 @@ namespace Emby.Server.Implementations.Net /// public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) { - if (ipAddress == null) throw new ArgumentNullException(nameof(ipAddress)); - if (ipAddress.Length == 0) throw new ArgumentException("ipAddress cannot be an empty string.", nameof(ipAddress)); - if (multicastTimeToLive <= 0) throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); - if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + if (ipAddress == null) + { + throw new ArgumentNullException(nameof(ipAddress)); + } + + if (ipAddress.Length == 0) + { + throw new ArgumentException("ipAddress cannot be an empty string.", nameof(ipAddress)); + } + + if (multicastTimeToLive <= 0) + { + throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); + } + + if (localPort < 0) + { + throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); + } var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); @@ -172,87 +194,13 @@ namespace Emby.Server.Implementations.Net } catch { - if (retVal != null) - retVal.Dispose(); + retVal?.Dispose(); throw; } } public Stream CreateNetworkStream(ISocket socket, bool ownsSocket) - { - var netSocket = (UdpSocket)socket; - - return new SocketStream(netSocket.Socket, ownsSocket); - } + => new NetworkStream(((UdpSocket)socket).Socket, ownsSocket); } - - public class SocketStream : Stream - { - private readonly Socket _socket; - - public SocketStream(Socket socket, bool ownsSocket) - { - _socket = socket; - } - - public override void Flush() - { - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _socket.Send(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginSend(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - _socket.EndSend(asyncResult); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _socket.Receive(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginReceive(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return _socket.EndReceive(asyncResult); - } - } - } diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index d488554860..6c55085c83 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -11,12 +11,15 @@ namespace Emby.Server.Implementations.Net // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS // Be careful to check any changes compile and work for all platform projects it is shared in. - public sealed class UdpSocket : DisposableManagedObjectBase, ISocket + public sealed class UdpSocket : ISocket, IDisposable { - private Socket _Socket; - private int _LocalPort; + private Socket _socket; + private int _localPort; + private bool _disposed = false; - public Socket Socket => _Socket; + public Socket Socket => _socket; + + public IpAddressInfo LocalIPAddress { get; } private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs() { @@ -35,11 +38,11 @@ namespace Emby.Server.Implementations.Net { if (socket == null) throw new ArgumentNullException(nameof(socket)); - _Socket = socket; - _LocalPort = localPort; + _socket = socket; + _localPort = localPort; LocalIPAddress = NetworkManager.ToIpAddressInfo(ip); - _Socket.Bind(new IPEndPoint(ip, _LocalPort)); + _socket.Bind(new IPEndPoint(ip, _localPort)); InitReceiveSocketAsyncEventArgs(); } @@ -101,32 +104,26 @@ namespace Emby.Server.Implementations.Net { if (socket == null) throw new ArgumentNullException(nameof(socket)); - _Socket = socket; - _Socket.Connect(NetworkManager.ToIPEndPoint(endPoint)); + _socket = socket; + _socket.Connect(NetworkManager.ToIPEndPoint(endPoint)); InitReceiveSocketAsyncEventArgs(); } - public IpAddressInfo LocalIPAddress - { - get; - private set; - } - public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback) { ThrowIfDisposed(); EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); - return _Socket.BeginReceiveFrom(buffer, offset, count, SocketFlags.None, ref receivedFromEndPoint, callback, buffer); + return _socket.BeginReceiveFrom(buffer, offset, count, SocketFlags.None, ref receivedFromEndPoint, callback, buffer); } public int Receive(byte[] buffer, int offset, int count) { ThrowIfDisposed(); - return _Socket.Receive(buffer, 0, buffer.Length, SocketFlags.None); + return _socket.Receive(buffer, 0, buffer.Length, SocketFlags.None); } public SocketReceiveResult EndReceive(IAsyncResult result) @@ -136,7 +133,7 @@ namespace Emby.Server.Implementations.Net var sender = new IPEndPoint(IPAddress.Any, 0); var remoteEndPoint = (EndPoint)sender; - var receivedBytes = _Socket.EndReceiveFrom(result, ref remoteEndPoint); + var receivedBytes = _socket.EndReceiveFrom(result, ref remoteEndPoint); var buffer = (byte[])result.AsyncState; @@ -236,37 +233,42 @@ namespace Emby.Server.Implementations.Net var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint); - return _Socket.BeginSendTo(buffer, offset, size, SocketFlags.None, ipEndPoint, callback, state); + return _socket.BeginSendTo(buffer, offset, size, SocketFlags.None, ipEndPoint, callback, state); } public int EndSendTo(IAsyncResult result) { ThrowIfDisposed(); - return _Socket.EndSendTo(result); + return _socket.EndSendTo(result); } - protected override void Dispose(bool disposing) + private void ThrowIfDisposed() { - if (disposing) + if (_disposed) { - var socket = _Socket; - if (socket != null) - socket.Dispose(); - - var tcs = _currentReceiveTaskCompletionSource; - if (tcs != null) - { - tcs.TrySetCanceled(); - } - var sendTcs = _currentSendTaskCompletionSource; - if (sendTcs != null) - { - sendTcs.TrySetCanceled(); - } + throw new ObjectDisposedException(nameof(UdpSocket)); } } + public void Dispose() + { + if (_disposed) + { + return; + } + + _socket?.Dispose(); + _currentReceiveTaskCompletionSource?.TrySetCanceled(); + _currentSendTaskCompletionSource?.TrySetCanceled(); + + _socket = null; + _currentReceiveTaskCompletionSource = null; + _currentSendTaskCompletionSource = null; + + _disposed = true; + } + private static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) { if (endpoint == null) diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 486d5e8a71..e41ad540ad 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -23,7 +23,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.LiveTv @@ -695,27 +694,36 @@ namespace MediaBrowser.Api.LiveTv private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; - private readonly IFileSystem _fileSystem; private readonly IAuthorizationContext _authContext; private readonly ISessionContext _sessionContext; - private ICryptoProvider _cryptographyProvider; - private IStreamHelper _streamHelper; - private IMediaSourceManager _mediaSourceManager; + private readonly ICryptoProvider _cryptographyProvider; + private readonly IStreamHelper _streamHelper; + private readonly IMediaSourceManager _mediaSourceManager; - public LiveTvService(ICryptoProvider crypto, IMediaSourceManager mediaSourceManager, IStreamHelper streamHelper, ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem, IAuthorizationContext authContext, ISessionContext sessionContext) + public LiveTvService( + ICryptoProvider crypto, + IMediaSourceManager mediaSourceManager, + IStreamHelper streamHelper, + ILiveTvManager liveTvManager, + IUserManager userManager, + IServerConfigurationManager config, + IHttpClient httpClient, + ILibraryManager libraryManager, + IDtoService dtoService, + IAuthorizationContext authContext, + ISessionContext sessionContext) { + _cryptographyProvider = crypto; + _mediaSourceManager = mediaSourceManager; + _streamHelper = streamHelper; _liveTvManager = liveTvManager; _userManager = userManager; _config = config; _httpClient = httpClient; _libraryManager = libraryManager; _dtoService = dtoService; - _fileSystem = fileSystem; _authContext = authContext; _sessionContext = sessionContext; - _cryptographyProvider = crypto; - _streamHelper = streamHelper; - _mediaSourceManager = mediaSourceManager; } public object Get(GetTunerHostTypes request) @@ -729,7 +737,7 @@ namespace MediaBrowser.Api.LiveTv var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId); var folders = _liveTvManager.GetRecordingFolders(user); - var returnArray = _dtoService.GetBaseItemDtos(folders.ToArray(), new DtoOptions(), user); + var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); var result = new QueryResult { @@ -754,7 +762,7 @@ namespace MediaBrowser.Api.LiveTv [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path) }; - return new ProgressiveFileCopier(_fileSystem, _streamHelper, path, outputHeaders, Logger) + return new ProgressiveFileCopier(_streamHelper, path, outputHeaders, Logger) { AllowEndOfFile = false }; diff --git a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs index 51552d928b..4c608d9a33 100644 --- a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs +++ b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Threading; @@ -11,22 +12,17 @@ namespace MediaBrowser.Api.LiveTv { public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders { - private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly string _path; private readonly Dictionary _outputHeaders; - const int StreamCopyToBufferSize = 81920; - - public long StartPosition { get; set; } public bool AllowEndOfFile = true; private readonly IDirectStreamProvider _directStreamProvider; private IStreamHelper _streamHelper; - public ProgressiveFileCopier(IFileSystem fileSystem, IStreamHelper streamHelper, string path, Dictionary outputHeaders, ILogger logger) + public ProgressiveFileCopier(IStreamHelper streamHelper, string path, Dictionary outputHeaders, ILogger logger) { - _fileSystem = fileSystem; _path = path; _outputHeaders = outputHeaders; _logger = logger; @@ -43,18 +39,6 @@ namespace MediaBrowser.Api.LiveTv public IDictionary Headers => _outputHeaders; - private Stream GetInputStream(bool allowAsyncFileRead) - { - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsyncFileRead) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - return _fileSystem.GetFileStream(_path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); - } - public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken) { if (_directStreamProvider != null) @@ -63,28 +47,23 @@ namespace MediaBrowser.Api.LiveTv return; } - var eofCount = 0; + var fileOptions = FileOptions.SequentialScan; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - var allowAsyncFileRead = true; - - using (var inputStream = GetInputStream(allowAsyncFileRead)) + if (Environment.OSVersion.Platform != PlatformID.Win32NT) { - if (StartPosition > 0) - { - inputStream.Position = StartPosition; - } + fileOptions |= FileOptions.Asynchronous; + } + using (var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions)) + { var emptyReadLimit = AllowEndOfFile ? 20 : 100; - + var eofCount = 0; while (eofCount < emptyReadLimit) { int bytesRead; bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false); - //var position = fs.Position; - //_logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); - if (bytesRead == 0) { eofCount++; diff --git a/MediaBrowser.Model/Net/HttpResponse.cs b/MediaBrowser.Model/Net/HttpResponse.cs deleted file mode 100644 index 286b1c0afd..0000000000 --- a/MediaBrowser.Model/Net/HttpResponse.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; - -namespace MediaBrowser.Model.Net -{ - public class HttpResponse : IDisposable - { - /// - /// Gets or sets the type of the content. - /// - /// The type of the content. - public string ContentType { get; set; } - - /// - /// Gets or sets the response URL. - /// - /// The response URL. - public string ResponseUrl { get; set; } - - /// - /// Gets or sets the content. - /// - /// The content. - public Stream Content { get; set; } - - /// - /// Gets or sets the status code. - /// - /// The status code. - public HttpStatusCode StatusCode { get; set; } - - /// - /// Gets or sets the length of the content. - /// - /// The length of the content. - public long? ContentLength { get; set; } - - /// - /// Gets or sets the headers. - /// - /// The headers. - public Dictionary Headers { get; set; } - - private readonly IDisposable _disposable; - - public HttpResponse(IDisposable disposable) - { - _disposable = disposable; - } - public HttpResponse() - { - } - - public void Dispose() - { - if (_disposable != null) - { - _disposable.Dispose(); - } - } - } -} diff --git a/MediaBrowser.Model/Net/IAcceptSocket.cs b/MediaBrowser.Model/Net/IAcceptSocket.cs deleted file mode 100644 index 2b21d3e660..0000000000 --- a/MediaBrowser.Model/Net/IAcceptSocket.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Net -{ - public class SocketCreateException : Exception - { - public SocketCreateException(string errorCode, Exception originalException) - : base(errorCode, originalException) - { - ErrorCode = errorCode; - } - - public string ErrorCode { get; private set; } - } -} From bf43dc00bb193fb4c7aca882aadbe086ed5a1815 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 13 Mar 2019 22:32:52 +0100 Subject: [PATCH 0215/6208] More warning fixes --- .../ApplicationHost.cs | 207 ++++++------ .../Diagnostics/CommonProcess.cs | 72 ++-- .../Library/LibraryManager.cs | 319 +++++++++--------- .../Localization/LocalizationManager.cs | 37 +- .../Session/SessionManager.cs | 242 +++++++------ .../Sorting/AlbumArtistComparer.cs | 2 +- .../Sorting/SeriesSortNameComparer.cs | 14 +- MediaBrowser.Api/BaseApiService.cs | 35 +- .../UserLibrary/ArtistsService.cs | 10 - .../Manager/ProviderManager.cs | 39 +-- .../Music/MusicBrainzAlbumProvider.cs | 24 +- .../Music/MusicBrainzArtistProvider.cs | 18 +- 12 files changed, 532 insertions(+), 487 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 1810064e69..94eaffa479 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -34,7 +34,6 @@ using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; -using Emby.Server.Implementations.Middleware; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; @@ -161,7 +160,7 @@ namespace Emby.Server.Implementations public event EventHandler> ApplicationUpdated; /// - /// Gets or sets a value indicating whether this instance has changes that require the entire application to restart. + /// Gets a value indicating whether this instance has changes that require the entire application to restart. /// /// true if this instance has pending application restart; otherwise, false. public bool HasPendingRestart { get; private set; } @@ -175,7 +174,7 @@ namespace Emby.Server.Implementations protected ILogger Logger { get; set; } /// - /// Gets or sets the plugins. + /// Gets the plugins. /// /// The plugins. public IPlugin[] Plugins { get; protected set; } @@ -187,13 +186,13 @@ namespace Emby.Server.Implementations public ILoggerFactory LoggerFactory { get; protected set; } /// - /// Gets the application paths. + /// Gets or sets the application paths. /// /// The application paths. protected ServerApplicationPaths ApplicationPaths { get; set; } /// - /// Gets all concrete types. + /// Gets or sets all concrete types. /// /// All concrete types. public Type[] AllConcreteTypes { get; protected set; } @@ -201,7 +200,7 @@ namespace Emby.Server.Implementations /// /// The disposable parts /// - protected readonly List DisposableParts = new List(); + protected readonly List _disposableParts = new List(); /// /// Gets the configuration manager. @@ -240,27 +239,33 @@ namespace Emby.Server.Implementations /// /// The user manager. public IUserManager UserManager { get; set; } + /// /// Gets or sets the library manager. /// /// The library manager. internal ILibraryManager LibraryManager { get; set; } + /// /// Gets or sets the directory watchers. /// /// The directory watchers. private ILibraryMonitor LibraryMonitor { get; set; } + /// /// Gets or sets the provider manager. /// /// The provider manager. private IProviderManager ProviderManager { get; set; } + /// /// Gets or sets the HTTP server. /// /// The HTTP server. private IHttpServer HttpServer { get; set; } + private IDtoService DtoService { get; set; } + public IImageProcessor ImageProcessor { get; set; } /// @@ -268,6 +273,7 @@ namespace Emby.Server.Implementations /// /// The media encoder. private IMediaEncoder MediaEncoder { get; set; } + private ISubtitleEncoder SubtitleEncoder { get; set; } private ISessionManager SessionManager { get; set; } @@ -277,6 +283,7 @@ namespace Emby.Server.Implementations public LocalizationManager LocalizationManager { get; set; } private IEncodingManager EncodingManager { get; set; } + private IChannelManager ChannelManager { get; set; } /// @@ -284,20 +291,29 @@ namespace Emby.Server.Implementations /// /// The user data repository. private IUserDataManager UserDataManager { get; set; } + private IUserRepository UserRepository { get; set; } + internal SqliteItemRepository ItemRepository { get; set; } private INotificationManager NotificationManager { get; set; } + private ISubtitleManager SubtitleManager { get; set; } + private IChapterManager ChapterManager { get; set; } + private IDeviceManager DeviceManager { get; set; } internal IUserViewManager UserViewManager { get; set; } private IAuthenticationRepository AuthenticationRepository { get; set; } + private ITVSeriesManager TVSeriesManager { get; set; } + private ICollectionManager CollectionManager { get; set; } + private IMediaSourceManager MediaSourceManager { get; set; } + private IPlaylistManager PlaylistManager { get; set; } private readonly IConfiguration _configuration; @@ -313,28 +329,37 @@ namespace Emby.Server.Implementations /// /// The zip client. protected IZipClient ZipClient { get; private set; } + protected IHttpResultFactory HttpResultFactory { get; private set; } + protected IAuthService AuthService { get; private set; } - public IStartupOptions StartupOptions { get; private set; } + public IStartupOptions StartupOptions { get; } internal IImageEncoder ImageEncoder { get; private set; } protected IProcessFactory ProcessFactory { get; private set; } + protected ICryptoProvider CryptographyProvider = new CryptographyProvider(); protected readonly IXmlSerializer XmlSerializer; protected ISocketFactory SocketFactory { get; private set; } + protected ITaskManager TaskManager { get; private set; } + public IHttpClient HttpClient { get; private set; } + protected INetworkManager NetworkManager { get; set; } + public IJsonSerializer JsonSerializer { get; private set; } + protected IIsoManager IsoManager { get; private set; } /// /// Initializes a new instance of the class. /// - public ApplicationHost(ServerApplicationPaths applicationPaths, + public ApplicationHost( + ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, @@ -395,7 +420,7 @@ namespace Emby.Server.Implementations _validAddressResults.Clear(); } - public string ApplicationVersion => typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); + public string ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); /// /// Gets the current application user agent @@ -404,13 +429,16 @@ namespace Emby.Server.Implementations public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion; private string _productName; + /// /// Gets the current application name /// /// The application name. - public string ApplicationProductName => _productName ?? (_productName = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName); + public string ApplicationProductName + => _productName ?? (_productName = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName); private DeviceId _deviceId; + public string SystemId { get @@ -441,15 +469,15 @@ namespace Emby.Server.Implementations /// /// Creates an instance of type and resolves all constructor dependencies /// - /// The type. - /// System.Object. + /// /// The type + /// T public T CreateInstance() => ActivatorUtilities.CreateInstance(_serviceProvider); /// /// Creates the instance safe. /// - /// The type information. + /// The type. /// System.Object. protected object CreateInstanceSafe(Type type) { @@ -468,14 +496,14 @@ namespace Emby.Server.Implementations /// /// Resolves this instance. /// - /// + /// The type /// ``0. public T Resolve() => _serviceProvider.GetService(); /// /// Gets the export types. /// - /// + /// The type /// IEnumerable{Type}. public IEnumerable GetExportTypes() { @@ -487,22 +515,22 @@ namespace Emby.Server.Implementations /// /// Gets the exports. /// - /// + /// The type /// if set to true [manage lifetime]. /// IEnumerable{``0}. public IEnumerable GetExports(bool manageLifetime = true) { var parts = GetExportTypes() - .Select(x => CreateInstanceSafe(x)) + .Select(CreateInstanceSafe) .Where(i => i != null) .Cast() .ToList(); // Convert to list so this isn't executed for each iteration if (manageLifetime) { - lock (DisposableParts) + lock (_disposableParts) { - DisposableParts.AddRange(parts.OfType()); + _disposableParts.AddRange(parts.OfType()); } } @@ -522,29 +550,20 @@ namespace Emby.Server.Implementations MediaEncoder.SetFFmpegPath(); - //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) - //{ - // if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - // { - // ServerConfigurationManager.Configuration.IsStartupWizardCompleted = false; - // ServerConfigurationManager.SaveConfiguration(); - // } - //} - Logger.LogInformation("ServerId: {0}", SystemId); - var entryPoints = GetExports(); + var entryPoints = GetExports().ToList(); var stopWatch = new Stopwatch(); stopWatch.Start(); - await Task.WhenAll(StartEntryPoints(entryPoints, true)); + await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); HttpServer.GlobalResponse = null; stopWatch.Restart(); - await Task.WhenAll(StartEntryPoints(entryPoints, false)); + await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); stopWatch.Stop(); } @@ -594,7 +613,7 @@ namespace Emby.Server.Implementations SetHttpLimit(); - await RegisterResources(serviceCollection); + await RegisterResources(serviceCollection).ConfigureAwait(false); FindParts(); @@ -631,7 +650,7 @@ namespace Emby.Server.Implementations }) .Build(); - await host.StartAsync(); + await host.StartAsync().ConfigureAwait(false); } private async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) @@ -729,8 +748,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ServerConfigurationManager); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory); - await LocalizationManager.LoadAll(); + LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory); + await LocalizationManager.LoadAll().ConfigureAwait(false); serviceCollection.AddSingleton(LocalizationManager); serviceCollection.AddSingleton(new BdInfoExaminer(FileSystemManager)); @@ -769,16 +788,19 @@ namespace Emby.Server.Implementations CertificateInfo = GetCertificateInfo(true); Certificate = GetCertificate(CertificateInfo); - HttpServer = new HttpListenerHost(this, + HttpServer = new HttpListenerHost( + this, LoggerFactory, ServerConfigurationManager, _configuration, NetworkManager, JsonSerializer, XmlSerializer, - CreateHttpListener()); + CreateHttpListener()) + { + GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading") + }; - HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); serviceCollection.AddSingleton(HttpServer); ImageProcessor = GetImageProcessor(); @@ -933,7 +955,7 @@ namespace Emby.Server.Implementations var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; var localCert = new X509Certificate2(certificateLocation, password); - //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; + // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) { Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation); @@ -1034,13 +1056,15 @@ namespace Emby.Server.Implementations HttpServer.Init(GetExports(false), GetExports(), GetUrlPrefixes()); - LibraryManager.AddParts(GetExports(), + LibraryManager.AddParts( + GetExports(), GetExports(), GetExports(), GetExports(), GetExports()); - ProviderManager.AddParts(GetExports(), + ProviderManager.AddParts( + GetExports(), GetExports(), GetExports(), GetExports(), @@ -1121,6 +1145,7 @@ namespace Emby.Server.Implementations } private CertificateInfo CertificateInfo { get; set; } + protected X509Certificate2 Certificate { get; private set; } private IEnumerable GetUrlPrefixes() @@ -1131,7 +1156,7 @@ namespace Emby.Server.Implementations { var prefixes = new List { - "http://"+i+":" + HttpPort + "/" + "http://" + i + ":" + HttpPort + "/" }; if (CertificateInfo != null) @@ -1160,30 +1185,12 @@ namespace Emby.Server.Implementations // Generate self-signed cert var certHost = GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns); var certPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.ProgramDataPath, "ssl", "cert_" + (certHost + "2").GetMD5().ToString("N") + ".pfx"); - var password = "embycert"; - - //if (generateCertificate) - //{ - // if (!File.Exists(certPath)) - // { - // FileSystemManager.CreateDirectory(FileSystemManager.GetDirectoryName(certPath)); - - // try - // { - // CertificateGenerator.CreateSelfSignCertificatePfx(certPath, certHost, password, Logger); - // } - // catch (Exception ex) - // { - // Logger.LogError(ex, "Error creating ssl cert"); - // return null; - // } - // } - //} + const string Password = "embycert"; return new CertificateInfo { Path = certPath, - Password = password + Password = Password }; } @@ -1218,9 +1225,9 @@ namespace Emby.Server.Implementations requiresRestart = true; } - var currentCertPath = CertificateInfo == null ? null : CertificateInfo.Path; + var currentCertPath = CertificateInfo?.Path; var newCertInfo = GetCertificateInfo(false); - var newCertPath = newCertInfo == null ? null : newCertInfo.Path; + var newCertPath = newCertInfo?.Path; if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase)) { @@ -1353,6 +1360,7 @@ namespace Emby.Server.Implementations /// /// Gets the system status. /// + /// The cancellation token /// SystemInfo. public async Task GetSystemInfo(CancellationToken cancellationToken) { @@ -1447,19 +1455,19 @@ namespace Emby.Server.Implementations public async Task GetWanApiUrl(CancellationToken cancellationToken) { - const string url = "http://ipv4.icanhazip.com"; + const string Url = "http://ipv4.icanhazip.com"; try { using (var response = await HttpClient.Get(new HttpRequestOptions { - Url = url, + Url = Url, LogErrorResponseBody = false, LogErrors = false, LogRequest = false, TimeoutMs = 10000, BufferContent = false, CancellationToken = cancellationToken - })) + }).ConfigureAwait(false)) { return GetLocalApiUrl(response.ReadToEnd().Trim()); } @@ -1547,10 +1555,12 @@ namespace Emby.Server.Implementations { return result; } + return null; } private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private async Task IsIpAddressValidAsync(IpAddressInfo address, CancellationToken cancellationToken) { if (address.Equals(IpAddressInfo.Loopback) || @@ -1567,26 +1577,26 @@ namespace Emby.Server.Implementations return cachedResult; } - var logPing = false; - #if DEBUG - logPing = true; + const bool LogPing = true; +#else + const bool LogPing = false; #endif try { - using (var response = await HttpClient.SendAsync(new HttpRequestOptions - { - Url = apiUrl, - LogErrorResponseBody = false, - LogErrors = logPing, - LogRequest = logPing, - TimeoutMs = 5000, - BufferContent = false, + using (var response = await HttpClient.SendAsync( + new HttpRequestOptions + { + Url = apiUrl, + LogErrorResponseBody = false, + LogErrors = LogPing, + LogRequest = LogPing, + TimeoutMs = 5000, + BufferContent = false, - CancellationToken = cancellationToken - - }, "POST").ConfigureAwait(false)) + CancellationToken = cancellationToken + }, "POST").ConfigureAwait(false)) { using (var reader = new StreamReader(response.Content)) { @@ -1651,6 +1661,7 @@ namespace Emby.Server.Implementations public event EventHandler HasUpdateAvailableChanged; private bool _hasUpdateAvailable; + public bool HasUpdateAvailable { get => _hasUpdateAvailable; @@ -1711,7 +1722,7 @@ namespace Emby.Server.Implementations var process = ProcessFactory.Create(new ProcessOptions { FileName = url, - //EnableRaisingEvents = true, + EnableRaisingEvents = true, UseShellExecute = true, ErrorDialog = false }); @@ -1746,26 +1757,25 @@ namespace Emby.Server.Implementations { Logger.LogInformation("Application has been updated to version {0}", package.versionStr); - ApplicationUpdated?.Invoke(this, new GenericEventArgs - { - Argument = package - }); + ApplicationUpdated?.Invoke( + this, + new GenericEventArgs() + { + Argument = package + }); NotifyPendingRestart(); } - private bool _disposed; + private bool _disposed = false; + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { - if (!_disposed) - { - _disposed = true; - - Dispose(true); - } + Dispose(true); + GC.SuppressFinalize(this); } /// @@ -1774,14 +1784,19 @@ namespace Emby.Server.Implementations /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { + if (_disposed) + { + return; + } + if (dispose) { var type = GetType(); Logger.LogInformation("Disposing {Type}", type.Name); - var parts = DisposableParts.Distinct().Where(i => i.GetType() != type).ToList(); - DisposableParts.Clear(); + var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList(); + _disposableParts.Clear(); foreach (var part in parts) { @@ -1797,6 +1812,8 @@ namespace Emby.Server.Implementations } } } + + _disposed = true; } } diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs index 2c4ef170dc..175a8f3ce4 100644 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs @@ -9,14 +9,14 @@ namespace Emby.Server.Implementations.Diagnostics { public class CommonProcess : IProcess { - public event EventHandler Exited; - - private readonly ProcessOptions _options; private readonly Process _process; + private bool _disposed = false; + private bool _hasExited; + public CommonProcess(ProcessOptions options) { - _options = options; + StartInfo = options; var startInfo = new ProcessStartInfo { @@ -27,10 +27,10 @@ namespace Emby.Server.Implementations.Diagnostics CreateNoWindow = options.CreateNoWindow, RedirectStandardError = options.RedirectStandardError, RedirectStandardInput = options.RedirectStandardInput, - RedirectStandardOutput = options.RedirectStandardOutput + RedirectStandardOutput = options.RedirectStandardOutput, + ErrorDialog = options.ErrorDialog }; - startInfo.ErrorDialog = options.ErrorDialog; if (options.IsHidden) { @@ -45,11 +45,22 @@ namespace Emby.Server.Implementations.Diagnostics if (options.EnableRaisingEvents) { _process.EnableRaisingEvents = true; - _process.Exited += _process_Exited; + _process.Exited += OnProcessExited; } } - private bool _hasExited; + public event EventHandler Exited; + + public ProcessOptions StartInfo { get; } + + public StreamWriter StandardInput => _process.StandardInput; + + public StreamReader StandardError => _process.StandardError; + + public StreamReader StandardOutput => _process.StandardOutput; + + public int ExitCode => _process.ExitCode; + private bool HasExited { get @@ -72,25 +83,6 @@ namespace Emby.Server.Implementations.Diagnostics } } - private void _process_Exited(object sender, EventArgs e) - { - _hasExited = true; - if (Exited != null) - { - Exited(this, e); - } - } - - public ProcessOptions StartInfo => _options; - - public StreamWriter StandardInput => _process.StandardInput; - - public StreamReader StandardError => _process.StandardError; - - public StreamReader StandardOutput => _process.StandardOutput; - - public int ExitCode => _process.ExitCode; - public void Start() { _process.Start(); @@ -108,7 +100,7 @@ namespace Emby.Server.Implementations.Diagnostics public Task WaitForExitAsync(int timeMs) { - //Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true. + // Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true. if (HasExited) { @@ -130,7 +122,29 @@ namespace Emby.Server.Implementations.Diagnostics public void Dispose() { - _process?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _process?.Dispose(); + } + + _disposed = true; + } + + private void OnProcessExited(object sender, EventArgs e) + { + _hasExited = true; + Exited?.Invoke(this, e); } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 6591d54c56..1673e37776 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -58,22 +58,23 @@ namespace Emby.Server.Implementations.Library private ILibraryPostScanTask[] PostscanTasks { get; set; } /// - /// Gets the intro providers. + /// Gets or sets the intro providers. /// /// The intro providers. private IIntroProvider[] IntroProviders { get; set; } /// - /// Gets the list of entity resolution ignore rules + /// Gets or sets the list of entity resolution ignore rules /// /// The entity resolution ignore rules. private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// - /// Gets the list of currently registered entity resolvers + /// Gets or sets the list of currently registered entity resolvers /// /// The entity resolvers enumerable. private IItemResolver[] EntityResolvers { get; set; } + private IMultiItemResolver[] MultiItemResolvers { get; set; } /// @@ -83,7 +84,7 @@ namespace Emby.Server.Implementations.Library private IBaseItemComparer[] Comparers { get; set; } /// - /// Gets the active item repository + /// Gets or sets the active item repository /// /// The item repository. public IItemRepository ItemRepository { get; set; } @@ -133,12 +134,14 @@ namespace Emby.Server.Implementations.Library private readonly Func _providerManagerFactory; private readonly Func _userviewManager; public bool IsScanRunning { get; private set; } + private IServerApplicationHost _appHost; /// /// The _library items cache /// private readonly ConcurrentDictionary _libraryItemsCache; + /// /// Gets the library items cache. /// @@ -150,7 +153,8 @@ namespace Emby.Server.Implementations.Library /// /// Initializes a new instance of the class. /// - /// The logger. + /// The application host + /// The logger factory. /// The task manager. /// The user manager. /// The configuration manager. @@ -167,6 +171,7 @@ namespace Emby.Server.Implementations.Library Func providerManagerFactory, Func userviewManager) { + _appHost = appHost; _logger = loggerFactory.CreateLogger(nameof(LibraryManager)); _taskManager = taskManager; _userManager = userManager; @@ -176,7 +181,7 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; - _appHost = appHost; + _libraryItemsCache = new ConcurrentDictionary(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -191,8 +196,9 @@ namespace Emby.Server.Implementations.Library /// The resolvers. /// The intro providers. /// The item comparers. - /// The postscan tasks. - public void AddParts(IEnumerable rules, + /// The post scan tasks. + public void AddParts( + IEnumerable rules, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, @@ -203,24 +209,19 @@ namespace Emby.Server.Implementations.Library MultiItemResolvers = EntityResolvers.OfType().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); - - PostscanTasks = postscanTasks.OrderBy(i => - { - var hasOrder = i as IHasOrder; - - return hasOrder == null ? 0 : hasOrder.Order; - - }).ToArray(); + PostscanTasks = postscanTasks.ToArray(); } /// /// The _root folder /// private volatile AggregateFolder _rootFolder; + /// /// The _root folder sync lock /// private readonly object _rootFolderSyncLock = new object(); + /// /// Gets the root folder. /// @@ -239,11 +240,13 @@ namespace Emby.Server.Implementations.Library } } } + return _rootFolder; } } private bool _wizardCompleted; + /// /// Records the configuration values. /// @@ -258,7 +261,7 @@ namespace Emby.Server.Implementations.Library /// /// The sender. /// The instance containing the event data. - void ConfigurationUpdated(object sender, EventArgs e) + private void ConfigurationUpdated(object sender, EventArgs e) { var config = ConfigurationManager.Configuration; @@ -335,12 +338,14 @@ namespace Emby.Server.Implementations.Library // channel no longer installed } } + options.DeleteFileLocation = false; } if (item is LiveTvProgram) { - _logger.LogDebug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + _logger.LogDebug( + "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -348,7 +353,8 @@ namespace Emby.Server.Implementations.Library } else { - _logger.LogInformation("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + _logger.LogInformation( + "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -488,12 +494,13 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(key)); } + if (type == null) { throw new ArgumentNullException(nameof(type)); } - if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath)) + if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) { // Try to normalize paths located underneath program-data in an attempt to make them more portable key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length) @@ -511,13 +518,11 @@ namespace Emby.Server.Implementations.Library return key.GetMD5(); } - public BaseItem ResolvePath(FileSystemMetadata fileInfo, - Folder parent = null) - { - return ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent); - } + public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) + => ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent); - private BaseItem ResolvePath(FileSystemMetadata fileInfo, + private BaseItem ResolvePath( + FileSystemMetadata fileInfo, IDirectoryService directoryService, IItemResolver[] resolvers, Folder parent = null, @@ -572,7 +577,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); - files = new FileSystemMetadata[] { }; + files = Array.Empty(); } else { @@ -600,13 +605,7 @@ namespace Emby.Server.Implementations.Library } public bool IgnoreFile(FileSystemMetadata file, BaseItem parent) - { - if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent))) - { - return true; - } - return false; - } + => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)); public List NormalizeRootPathList(IEnumerable paths) { @@ -646,7 +645,8 @@ namespace Emby.Server.Implementations.Library return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); } - public IEnumerable ResolvePaths(IEnumerable files, + public IEnumerable ResolvePaths( + IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, @@ -672,6 +672,7 @@ namespace Emby.Server.Implementations.Library { ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService); } + items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions)); return items; } @@ -681,7 +682,8 @@ namespace Emby.Server.Implementations.Library return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions); } - private IEnumerable ResolveFileList(IEnumerable fileList, + private IEnumerable ResolveFileList( + IEnumerable fileList, IDirectoryService directoryService, Folder parent, string collectionType, @@ -766,6 +768,7 @@ namespace Emby.Server.Implementations.Library private volatile UserRootFolder _userRootFolder; private readonly object _syncLock = new object(); + public Folder GetUserRootFolder() { if (_userRootFolder == null) @@ -810,8 +813,6 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(path)); } - //_logger.LogInformation("FindByPath {0}", path); - var query = new InternalItemsQuery { Path = path, @@ -885,7 +886,6 @@ namespace Emby.Server.Implementations.Library /// /// The value. /// Task{Year}. - /// public Year GetYear(int value) { if (value <= 0) @@ -1027,20 +1027,25 @@ namespace Emby.Server.Implementations.Library private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) { - var rootChildren = RootFolder.Children.ToList(); - rootChildren = GetUserRootFolder().Children.ToList(); - await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further - await RootFolder.ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: false); + await RootFolder.ValidateChildren( + new SimpleProgress(), + cancellationToken, + new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + recursive: false).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: false).ConfigureAwait(false); + await GetUserRootFolder().ValidateChildren( + new SimpleProgress(), + cancellationToken, + new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + recursive: false).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var folder in GetUserRootFolder().Children.OfType().ToList()) + foreach (var folder in GetUserRootFolder().Children.OfType()) { await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); } @@ -1204,7 +1209,7 @@ namespace Emby.Server.Implementations.Library private string GetCollectionType(string path) { return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) - .Select(i => Path.GetFileNameWithoutExtension(i)) + .Select(Path.GetFileNameWithoutExtension) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } @@ -1218,7 +1223,7 @@ namespace Emby.Server.Implementations.Library { if (id == Guid.Empty) { - throw new ArgumentException(nameof(id), "Guid can't be empty"); + throw new ArgumentException("Guid can't be empty", nameof(id)); } if (LibraryItemsCache.TryGetValue(id, out BaseItem item)) @@ -1386,17 +1391,7 @@ namespace Emby.Server.Implementations.Library var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList(); - if (parents.All(i => - { - if (i is ICollectionFolder || i is UserView) - { - return true; - } - - //_logger.LogDebug("Query requires ancestor query due to type: " + i.GetType().Name); - return false; - - })) + if (parents.All(i => i is ICollectionFolder || i is UserView)) { // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); @@ -1452,17 +1447,7 @@ namespace Emby.Server.Implementations.Library private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List parents) { - if (parents.All(i => - { - if (i is ICollectionFolder || i is UserView) - { - return true; - } - - //_logger.LogDebug("Query requires ancestor query due to type: " + i.GetType().Name); - return false; - - })) + if (parents.All(i => i is ICollectionFolder || i is UserView)) { // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); @@ -1511,11 +1496,9 @@ namespace Emby.Server.Implementations.Library private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) { - var view = item as UserView; - - if (view != null) + if (item is UserView view) { - if (string.Equals(view.ViewType, CollectionType.LiveTv)) + if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal)) { return new[] { view.Id }; } @@ -1528,8 +1511,10 @@ namespace Emby.Server.Implementations.Library { return GetTopParentIdsForQuery(displayParent, user); } + return Array.Empty(); } + if (!view.ParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.ParentId); @@ -1537,6 +1522,7 @@ namespace Emby.Server.Implementations.Library { return GetTopParentIdsForQuery(displayParent, user); } + return Array.Empty(); } @@ -1550,11 +1536,11 @@ namespace Emby.Server.Implementations.Library .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } + return Array.Empty(); } - var collectionFolder = item as CollectionFolder; - if (collectionFolder != null) + if (item is CollectionFolder collectionFolder) { return collectionFolder.PhysicalFolderIds; } @@ -1564,6 +1550,7 @@ namespace Emby.Server.Implementations.Library { return new[] { topParent.Id }; } + return Array.Empty(); } @@ -1760,19 +1747,16 @@ namespace Emby.Server.Implementations.Library { var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)); - if (comparer != null) + // If it requires a user, create a new one, and assign the user + if (comparer is IUserBaseItemComparer) { - // If it requires a user, create a new one, and assign the user - if (comparer is IUserBaseItemComparer) - { - var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType()); + var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType()); - userComparer.User = user; - userComparer.UserManager = _userManager; - userComparer.UserDataRepository = _userDataRepository; + userComparer.User = user; + userComparer.UserManager = _userManager; + userComparer.UserDataRepository = _userDataRepository; - return userComparer; - } + return userComparer; } return comparer; @@ -1783,7 +1767,6 @@ namespace Emby.Server.Implementations.Library /// /// The item. /// The parent item. - /// Task. public void CreateItem(BaseItem item, BaseItem parent) { CreateItems(new[] { item }, parent, CancellationToken.None); @@ -1793,20 +1776,23 @@ namespace Emby.Server.Implementations.Library /// Creates the items. /// /// The items. + /// The parent item /// The cancellation token. - /// Task. public void CreateItems(IEnumerable items, BaseItem parent, CancellationToken cancellationToken) { - ItemRepository.SaveItems(items, cancellationToken); + // Don't iterate multiple times + var itemsList = items.ToList(); - foreach (var item in items) + ItemRepository.SaveItems(itemsList, cancellationToken); + + foreach (var item in itemsList) { RegisterItem(item); } if (ItemAdded != null) { - foreach (var item in items) + foreach (var item in itemsList) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) @@ -1816,11 +1802,13 @@ namespace Emby.Server.Implementations.Library try { - ItemAdded(this, new ItemChangeEventArgs - { - Item = item, - Parent = parent ?? item.GetParent() - }); + ItemAdded( + this, + new ItemChangeEventArgs + { + Item = item, + Parent = parent ?? item.GetParent() + }); } catch (Exception ex) { @@ -1842,7 +1830,10 @@ namespace Emby.Server.Implementations.Library /// public void UpdateItems(IEnumerable items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - foreach (var item in items) + // Don't iterate multiple times + var itemsList = items.ToList(); + + foreach (var item in itemsList) { if (item.IsFileProtocol) { @@ -1854,14 +1845,11 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } - //var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; - //_logger.LogDebug("Saving {0} to database.", logName); - - ItemRepository.SaveItems(items, cancellationToken); + ItemRepository.SaveItems(itemsList, cancellationToken); if (ItemUpdated != null) { - foreach (var item in items) + foreach (var item in itemsList) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) @@ -1871,12 +1859,14 @@ namespace Emby.Server.Implementations.Library try { - ItemUpdated(this, new ItemChangeEventArgs - { - Item = item, - Parent = parent, - UpdateReason = updateReason - }); + ItemUpdated( + this, + new ItemChangeEventArgs + { + Item = item, + Parent = parent, + UpdateReason = updateReason + }); } catch (Exception ex) { @@ -1890,9 +1880,9 @@ namespace Emby.Server.Implementations.Library /// Updates the item. /// /// The item. + /// The parent item. /// The update reason. /// The cancellation token. - /// Task. public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { UpdateItems(new [] { item }, parent, updateReason, cancellationToken); @@ -1902,17 +1892,20 @@ namespace Emby.Server.Implementations.Library /// Reports the item removed. /// /// The item. + /// The parent item. public void ReportItemRemoved(BaseItem item, BaseItem parent) { if (ItemRemoved != null) { try { - ItemRemoved(this, new ItemChangeEventArgs - { - Item = item, - Parent = parent - }); + ItemRemoved( + this, + new ItemChangeEventArgs + { + Item = item, + Parent = parent + }); } catch (Exception ex) { @@ -2038,8 +2031,7 @@ namespace Emby.Server.Implementations.Library public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) { - var collectionFolder = item as ICollectionFolder; - if (collectionFolder != null) + if (item is ICollectionFolder collectionFolder) { return collectionFolder.CollectionType; } @@ -2049,13 +2041,11 @@ namespace Emby.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); - if (nameValuePair != null) - { - return nameValuePair.Value; - } - - return null; + var nameValuePair = ConfigurationManager.Configuration.ContentTypes + .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) + || (inherit && !string.IsNullOrEmpty(i.Name) + && _fileSystem.ContainsSubPath(i.Name, path))); + return nameValuePair?.Value; } private string GetTopFolderContentType(BaseItem item) @@ -2072,6 +2062,7 @@ namespace Emby.Server.Implementations.Library { break; } + item = parent; } @@ -2083,9 +2074,9 @@ namespace Emby.Server.Implementations.Library } private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); - //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1); - public UserView GetNamedView(User user, + public UserView GetNamedView( + User user, string name, string viewType, string sortName) @@ -2093,13 +2084,15 @@ namespace Emby.Server.Implementations.Library return GetNamedView(user, name, Guid.Empty, viewType, sortName); } - public UserView GetNamedView(string name, + public UserView GetNamedView( + string name, string viewType, string sortName) { - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, - "views", - _fileSystem.GetValidFilename(viewType)); + var path = Path.Combine( + ConfigurationManager.ApplicationPaths.InternalMetadataPath, + "views", + _fileSystem.GetValidFilename(viewType)); var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView)); @@ -2135,7 +2128,8 @@ namespace Emby.Server.Implementations.Library return item; } - public UserView GetNamedView(User user, + public UserView GetNamedView( + User user, string name, Guid parentId, string viewType, @@ -2164,10 +2158,10 @@ namespace Emby.Server.Implementations.Library Name = name, ViewType = viewType, ForcedSortName = sortName, - UserId = user.Id + UserId = user.Id, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; CreateItem(item, null); @@ -2184,20 +2178,24 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - // Need to force save to increment DateLastSaved - ForceSave = true + _providerManagerFactory().QueueRefresh( + item.Id, + new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + { + // Need to force save to increment DateLastSaved + ForceSave = true - }, RefreshPriority.Normal); + }, + RefreshPriority.Normal); } return item; } - public UserView GetShadowView(BaseItem parent, - string viewType, - string sortName) + public UserView GetShadowView( + BaseItem parent, + string viewType, + string sortName) { if (parent == null) { @@ -2248,18 +2246,21 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - // Need to force save to increment DateLastSaved - ForceSave = true - - }, RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh( + item.Id, + new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }, + RefreshPriority.Normal); } return item; } - public UserView GetNamedView(string name, + public UserView GetNamedView( + string name, Guid parentId, string viewType, string sortName, @@ -2322,17 +2323,21 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - // Need to force save to increment DateLastSaved - ForceSave = true - }, RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh( + item.Id, + new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }, + RefreshPriority.Normal); } return item; } - public void AddExternalSubtitleStreams(List streams, + public void AddExternalSubtitleStreams( + List streams, string videoPath, string[] files) { @@ -2436,6 +2441,7 @@ namespace Emby.Server.Implementations.Library { changed = true; } + episode.IndexNumber = episodeInfo.EpisodeNumber; } @@ -2445,6 +2451,7 @@ namespace Emby.Server.Implementations.Library { changed = true; } + episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; } @@ -2454,6 +2461,7 @@ namespace Emby.Server.Implementations.Library { changed = true; } + episode.ParentIndexNumber = episodeInfo.SeasonNumber; } } @@ -2483,6 +2491,7 @@ namespace Emby.Server.Implementations.Library private NamingOptions _namingOptions; private string[] _videoFileExtensions; + private NamingOptions GetNamingOptionsInternal() { if (_namingOptions == null) @@ -2679,7 +2688,7 @@ namespace Emby.Server.Implementations.Library var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase); var changed = false; - if (!string.Equals(newPath, path)) + if (!string.Equals(newPath, path, StringComparison.Ordinal)) { if (to.IndexOf('/') != -1) { @@ -2803,6 +2812,7 @@ namespace Emby.Server.Implementations.Library { continue; } + throw; } } @@ -2907,6 +2917,7 @@ namespace Emby.Server.Implementations.Library } private const string ShortcutFileExtension = ".mblink"; + public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) { AddMediaPathInternal(virtualFolderName, pathInfo, true); @@ -2923,7 +2934,7 @@ namespace Emby.Server.Implementations.Library if (string.IsNullOrWhiteSpace(path)) { - throw new ArgumentNullException(nameof(path)); + throw new ArgumentException(nameof(path)); } if (!Directory.Exists(path)) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 762649b716..8c49b64055 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -35,7 +34,6 @@ namespace Emby.Server.Implementations.Localization private readonly Dictionary> _allParentalRatings = new Dictionary>(StringComparer.OrdinalIgnoreCase); - private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly ILogger _logger; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; @@ -44,40 +42,38 @@ namespace Emby.Server.Implementations.Localization /// Initializes a new instance of the class. /// /// The configuration manager. - /// The file system. /// The json serializer. + /// The logger factory public LocalizationManager( IServerConfigurationManager configurationManager, - IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory) { _configurationManager = configurationManager; - _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; _logger = loggerFactory.CreateLogger(nameof(LocalizationManager)); } public async Task LoadAll() { - const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings."; + const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings."; // Extract from the assembly foreach (var resource in _assembly.GetManifestResourceNames()) { - if (!resource.StartsWith(ratingsResource)) + if (!resource.StartsWith(RatingsResource, StringComparison.Ordinal)) { continue; } - string countryCode = resource.Substring(ratingsResource.Length, 2); + string countryCode = resource.Substring(RatingsResource.Length, 2); var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); using (var str = _assembly.GetManifestResourceStream(resource)) using (var reader = new StreamReader(str)) { string line; - while ((line = await reader.ReadLineAsync()) != null) + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) { if (string.IsNullOrWhiteSpace(line)) { @@ -102,7 +98,7 @@ namespace Emby.Server.Implementations.Localization _allParentalRatings[countryCode] = dict; } - await LoadCultures(); + await LoadCultures().ConfigureAwait(false); } public string NormalizeFormKD(string text) @@ -121,14 +117,14 @@ namespace Emby.Server.Implementations.Localization { List list = new List(); - const string path = "Emby.Server.Implementations.Localization.iso6392.txt"; + const string ResourcePath = "Emby.Server.Implementations.Localization.iso6392.txt"; - using (var stream = _assembly.GetManifestResourceStream(path)) + using (var stream = _assembly.GetManifestResourceStream(ResourcePath)) using (var reader = new StreamReader(stream)) { while (!reader.EndOfStream) { - var line = await reader.ReadLineAsync(); + var line = await reader.ReadLineAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(line)) { @@ -154,11 +150,11 @@ namespace Emby.Server.Implementations.Localization string[] threeletterNames; if (string.IsNullOrWhiteSpace(parts[1])) { - threeletterNames = new [] { parts[0] }; + threeletterNames = new[] { parts[0] }; } else { - threeletterNames = new [] { parts[0], parts[1] }; + threeletterNames = new[] { parts[0], parts[1] }; } list.Add(new CultureDto @@ -218,6 +214,7 @@ namespace Emby.Server.Implementations.Localization /// Gets the ratings. /// /// The country code. + /// The ratings private Dictionary GetRatings(string countryCode) { _allParentalRatings.TryGetValue(countryCode, out var value); @@ -227,9 +224,12 @@ namespace Emby.Server.Implementations.Localization private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; + /// /// /// Gets the rating level. /// + /// Rating field + /// The rating level> public int? GetRatingLevel(string rating) { if (string.IsNullOrEmpty(rating)) @@ -301,6 +301,7 @@ namespace Emby.Server.Implementations.Localization { culture = _configurationManager.Configuration.UICulture; } + if (string.IsNullOrEmpty(culture)) { culture = DefaultCulture; @@ -346,8 +347,8 @@ namespace Emby.Server.Implementations.Localization var namespaceName = GetType().Namespace + "." + prefix; - await CopyInto(dictionary, namespaceName + "." + baseFilename); - await CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture)); + await CopyInto(dictionary, namespaceName + "." + baseFilename).ConfigureAwait(false); + await CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture)).ConfigureAwait(false); return dictionary; } @@ -359,7 +360,7 @@ namespace Emby.Server.Implementations.Localization // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain if (stream != null) { - var dict = await _jsonSerializer.DeserializeFromStreamAsync>(stream); + var dict = await _jsonSerializer.DeserializeFromStreamAsync>(stream).ConfigureAwait(false); foreach (var key in dict.Keys) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 03e7b26545..985748caf2 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -116,14 +116,14 @@ namespace Emby.Server.Implementations.Session _authRepo = authRepo; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; - _deviceManager.DeviceOptionsUpdated += _deviceManager_DeviceOptionsUpdated; + _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } - private void _deviceManager_DeviceOptionsUpdated(object sender, GenericEventArgs> e) + private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs> e) { foreach (var session in Sessions) { - if (string.Equals(session.DeviceId, e.Argument.Item1)) + if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal)) { if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName)) { @@ -138,11 +138,29 @@ namespace Emby.Server.Implementations.Session } } - private bool _disposed; + private bool _disposed = false; + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // TODO: dispose stuff + } + + _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; + _disposed = true; - _deviceManager.DeviceOptionsUpdated -= _deviceManager_DeviceOptionsUpdated; } public void CheckDisposed() @@ -157,7 +175,7 @@ namespace Emby.Server.Implementations.Session /// Gets all connections. /// /// All connections. - public IEnumerable Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList(); + public IEnumerable Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); private void OnSessionStarted(SessionInfo info) { @@ -171,20 +189,27 @@ namespace Emby.Server.Implementations.Session } } - EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs - { - SessionInfo = info - - }, _logger); + EventHelper.QueueEventIfNotNull( + SessionStarted, + this, + new SessionEventArgs + { + SessionInfo = info + }, + _logger); } private void OnSessionEnded(SessionInfo info) { - EventHelper.QueueEventIfNotNull(SessionEnded, this, new SessionEventArgs - { - SessionInfo = info + EventHelper.QueueEventIfNotNull( + SessionEnded, + this, + new SessionEventArgs + { + SessionInfo = info - }, _logger); + }, + _logger); info.Dispose(); } @@ -192,9 +217,6 @@ namespace Emby.Server.Implementations.Session public void UpdateDeviceName(string sessionId, string deviceName) { var session = GetSession(sessionId); - - var key = GetSessionKey(session.Client, session.DeviceId); - if (session != null) { session.DeviceName = deviceName; @@ -210,10 +232,10 @@ namespace Emby.Server.Implementations.Session /// Name of the device. /// The remote end point. /// The user. - /// Task. + /// SessionInfo. /// user - /// - public SessionInfo LogSessionActivity(string appName, + public SessionInfo LogSessionActivity( + string appName, string appVersion, string deviceId, string deviceName, @@ -226,10 +248,12 @@ namespace Emby.Server.Implementations.Session { throw new ArgumentNullException(nameof(appName)); } + if (string.IsNullOrEmpty(appVersion)) { throw new ArgumentNullException(nameof(appVersion)); } + if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException(nameof(deviceId)); @@ -260,10 +284,12 @@ namespace Emby.Server.Implementations.Session if ((activityDate - lastActivityDate).TotalSeconds > 10) { - SessionActivity?.Invoke(this, new SessionEventArgs - { - SessionInfo = session - }); + SessionActivity?.Invoke( + this, + new SessionEventArgs + { + SessionInfo = session + }); } return session; @@ -304,6 +330,7 @@ namespace Emby.Server.Implementations.Session /// /// Updates the now playing item id. /// + /// Task. private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) { if (string.IsNullOrEmpty(info.MediaSourceId)) @@ -418,7 +445,7 @@ namespace Emby.Server.Implementations.Session }); sessionInfo.UserId = user == null ? Guid.Empty : user.Id; - sessionInfo.UserName = user == null ? null : user.Name; + sessionInfo.UserName = user?.Name; sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); sessionInfo.RemoteEndPoint = remoteEndPoint; sessionInfo.Client = appName; @@ -432,7 +459,7 @@ namespace Emby.Server.Implementations.Session if (user == null) { - sessionInfo.AdditionalUsers = new SessionUserInfo[] { }; + sessionInfo.AdditionalUsers = Array.Empty(); } return sessionInfo; @@ -449,9 +476,9 @@ namespace Emby.Server.Implementations.Session ServerId = _appHost.SystemId }; - var username = user == null ? null : user.Name; + var username = user?.Name; - sessionInfo.UserId = user == null ? Guid.Empty : user.Id; + sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = username; sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); sessionInfo.RemoteEndPoint = remoteEndPoint; @@ -508,6 +535,7 @@ namespace Emby.Server.Implementations.Session _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); } } + private void StopIdleCheckTimer() { if (_idleTimer != null) @@ -539,9 +567,9 @@ namespace Emby.Server.Implementations.Session Item = session.NowPlayingItem, ItemId = session.NowPlayingItem == null ? Guid.Empty : session.NowPlayingItem.Id, SessionId = session.Id, - MediaSourceId = session.PlayState == null ? null : session.PlayState.MediaSourceId, - PositionTicks = session.PlayState == null ? null : session.PlayState.PositionTicks - }); + MediaSourceId = session.PlayState?.MediaSourceId, + PositionTicks = session.PlayState?.PositionTicks + }).ConfigureAwait(false); } catch (Exception ex) { @@ -616,18 +644,22 @@ namespace Emby.Server.Implementations.Session // Nothing to save here // Fire events to inform plugins - EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session + EventHelper.QueueEventIfNotNull( + PlaybackStart, + this, + new PlaybackProgressEventArgs + { + Item = libraryItem, + Users = users, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session - }, _logger); + }, + _logger); StartIdleCheckTimer(); } @@ -667,6 +699,7 @@ namespace Emby.Server.Implementations.Session /// /// Used to report playback progress for an item /// + /// Task. public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated) { CheckDisposed(); @@ -695,21 +728,23 @@ namespace Emby.Server.Implementations.Session } } - PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = session.PlayState.PositionTicks, - MediaSourceId = session.PlayState.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - IsPaused = info.IsPaused, - PlaySessionId = info.PlaySessionId, - IsAutomated = isAutomated, - Session = session - }); + PlaybackProgress?.Invoke( + this, + new PlaybackProgressEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = session.PlayState.PositionTicks, + MediaSourceId = session.PlayState.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + IsPaused = info.IsPaused, + PlaySessionId = info.PlaySessionId, + IsAutomated = isAutomated, + Session = session + }); if (!isAutomated) { @@ -830,8 +865,7 @@ namespace Emby.Server.Implementations.Session { MediaSourceInfo mediaSource = null; - var hasMediaSources = libraryItem as IHasMediaSources; - if (hasMediaSources != null) + if (libraryItem is IHasMediaSources hasMediaSources) { mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); } @@ -848,7 +882,8 @@ namespace Emby.Server.Implementations.Session { var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown"; - _logger.LogInformation("Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms", + _logger.LogInformation( + "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms", session.Client, session.ApplicationVersion, info.Item.Name, @@ -887,20 +922,24 @@ namespace Emby.Server.Implementations.Session } } - EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackStopEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = info.PositionTicks, - PlayedToCompletion = playedToCompletion, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session + EventHelper.QueueEventIfNotNull( + PlaybackStopped, + this, + new PlaybackStopEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = info.PositionTicks, + PlayedToCompletion = playedToCompletion, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session - }, _logger); + }, + _logger); } private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) @@ -936,11 +975,10 @@ namespace Emby.Server.Implementations.Session /// The session identifier. /// if set to true [throw on missing]. /// SessionInfo. - /// + /// sessionId private SessionInfo GetSession(string sessionId, bool throwOnMissing = true) { - var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId)); - + var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal)); if (session == null && throwOnMissing) { throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId)); @@ -952,7 +990,7 @@ namespace Emby.Server.Implementations.Session private SessionInfo GetSessionToRemoteControl(string sessionId) { // Accept either device id or session id - var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId)); + var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal)); if (session == null) { @@ -1061,10 +1099,12 @@ namespace Emby.Server.Implementations.Session var series = episode.Series; if (series != null) { - var episodes = series.GetEpisodes(user, new DtoOptions(false) - { - EnableImages = false - }) + var episodes = series.GetEpisodes( + user, + new DtoOptions(false) + { + EnableImages = false + }) .Where(i => !i.IsVirtualItem) .SkipWhile(i => i.Id != episode.Id) .ToList(); @@ -1100,9 +1140,7 @@ namespace Emby.Server.Implementations.Session return new List(); } - var byName = item as IItemByName; - - if (byName != null) + if (item is IItemByName byName) { return byName.GetTaggedItems(new InternalItemsQuery(user) { @@ -1152,7 +1190,7 @@ namespace Emby.Server.Implementations.Session if (item == null) { - _logger.LogError("A non-existant item Id {0} was passed into TranslateItemForInstantMix", id); + _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id); return new List(); } @@ -1163,13 +1201,15 @@ namespace Emby.Server.Implementations.Session { var generalCommand = new GeneralCommand { - Name = GeneralCommandType.DisplayContent.ToString() + Name = GeneralCommandType.DisplayContent.ToString(), + Arguments = + { + ["ItemId"] = command.ItemId, + ["ItemName"] = command.ItemName, + ["ItemType"] = command.ItemType + } }; - generalCommand.Arguments["ItemId"] = command.ItemId; - generalCommand.Arguments["ItemName"] = command.ItemName; - generalCommand.Arguments["ItemType"] = command.ItemType; - return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken); } @@ -1410,7 +1450,8 @@ namespace Emby.Server.Implementations.Session var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); - var session = LogSessionActivity(request.App, + var session = LogSessionActivity( + request.App, request.AppVersion, request.DeviceId, request.DeviceName, @@ -1454,9 +1495,9 @@ namespace Emby.Server.Implementations.Session { Logout(auth); } - catch + catch (Exception ex) { - + _logger.LogError(ex, "Error while logging out."); } } } @@ -1572,7 +1613,8 @@ namespace Emby.Server.Implementations.Session ReportCapabilities(session, capabilities, true); } - private void ReportCapabilities(SessionInfo session, + private void ReportCapabilities( + SessionInfo session, ClientCapabilities capabilities, bool saveCapabilities) { @@ -1580,10 +1622,12 @@ namespace Emby.Server.Implementations.Session if (saveCapabilities) { - CapabilitiesChanged?.Invoke(this, new SessionEventArgs - { - SessionInfo = session - }); + CapabilitiesChanged?.Invoke( + this, + new SessionEventArgs + { + SessionInfo = session + }); try { diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 535f123f90..0804b01fca 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting { var audio = x as IHasAlbumArtist; - return audio != null ? audio.AlbumArtists.FirstOrDefault() : null; + return audio?.AlbumArtists.FirstOrDefault(); } /// diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 46e0dd9186..504b6d2838 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -18,17 +18,17 @@ namespace Emby.Server.Implementations.Sorting return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); } - private static string GetValue(BaseItem item) - { - var hasSeries = item as IHasSeries; - - return hasSeries != null ? hasSeries.FindSeriesSortName() : null; - } - /// /// Gets the name. /// /// The name. public string Name => ItemSortBy.SeriesSortName; + + private static string GetValue(BaseItem item) + { + var hasSeries = item as IHasSeries; + + return hasSeries?.FindSeriesSortName(); + } } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 69673a49c1..49f8c6ace0 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -165,6 +165,7 @@ namespace MediaBrowser.Api { options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value; } + if (hasDtoOptions.EnableUserData.HasValue) { options.EnableUserData = hasDtoOptions.EnableUserData.Value; @@ -307,7 +308,7 @@ namespace MediaBrowser.Api return pathInfo[index]; } - private string[] Parse(string pathUri) + private static string[] Parse(string pathUri) { var actionParts = pathUri.Split(new[] { "://" }, StringSplitOptions.None); @@ -329,38 +330,32 @@ namespace MediaBrowser.Api /// protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager, DtoOptions dtoOptions) { - BaseItem item; - - if (type.IndexOf("Person", StringComparison.OrdinalIgnoreCase) == 0) + if (type.Equals("Person", StringComparison.OrdinalIgnoreCase)) { - item = GetPerson(name, libraryManager, dtoOptions); + return GetPerson(name, libraryManager, dtoOptions); } - else if (type.IndexOf("Artist", StringComparison.OrdinalIgnoreCase) == 0) + else if (type.Equals("Artist", StringComparison.OrdinalIgnoreCase)) { - item = GetArtist(name, libraryManager, dtoOptions); + return GetArtist(name, libraryManager, dtoOptions); } - else if (type.IndexOf("Genre", StringComparison.OrdinalIgnoreCase) == 0) + else if (type.Equals("Genre", StringComparison.OrdinalIgnoreCase)) { - item = GetGenre(name, libraryManager, dtoOptions); + return GetGenre(name, libraryManager, dtoOptions); } - else if (type.IndexOf("MusicGenre", StringComparison.OrdinalIgnoreCase) == 0) + else if (type.Equals("MusicGenre", StringComparison.OrdinalIgnoreCase)) { - item = GetMusicGenre(name, libraryManager, dtoOptions); + return GetMusicGenre(name, libraryManager, dtoOptions); } - else if (type.IndexOf("Studio", StringComparison.OrdinalIgnoreCase) == 0) + else if (type.Equals("Studio", StringComparison.OrdinalIgnoreCase)) { - item = GetStudio(name, libraryManager, dtoOptions); + return GetStudio(name, libraryManager, dtoOptions); } - else if (type.IndexOf("Year", StringComparison.OrdinalIgnoreCase) == 0) + else if (type.Equals("Year", StringComparison.OrdinalIgnoreCase)) { - item = libraryManager.GetYear(int.Parse(name)); - } - else - { - throw new ArgumentException(); + return libraryManager.GetYear(int.Parse(name)); } - return item; + throw new ArgumentException("Invalid type", nameof(type)); } } } diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index 7a8455ff26..a30f8adfed 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -87,11 +87,6 @@ namespace MediaBrowser.Api.UserLibrary /// System.Object. public object Get(GetArtists request) { - if (string.IsNullOrWhiteSpace(request.IncludeItemTypes)) - { - //request.IncludeItemTypes = "Audio,MusicVideo"; - } - return GetResultSlim(request); } @@ -102,11 +97,6 @@ namespace MediaBrowser.Api.UserLibrary /// System.Object. public object Get(GetAlbumArtists request) { - if (string.IsNullOrWhiteSpace(request.IncludeItemTypes)) - { - //request.IncludeItemTypes = "Audio,MusicVideo"; - } - var result = GetResultSlim(request); return ToOptimizedResult(result); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f26087fda3..860ea13cf6 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -942,10 +942,7 @@ namespace MediaBrowser.Providers.Manager _activeRefreshes[id] = 0; } - if (RefreshStarted != null) - { - RefreshStarted(this, new GenericEventArgs(item)); - } + RefreshStarted?.Invoke(this, new GenericEventArgs(item)); } public void OnRefreshComplete(BaseItem item) @@ -956,10 +953,7 @@ namespace MediaBrowser.Providers.Manager _activeRefreshes.Remove(item.Id); } - if (RefreshCompleted != null) - { - RefreshCompleted(this, new GenericEventArgs(item)); - } + RefreshCompleted?.Invoke(this, new GenericEventArgs(item)); } public double? GetRefreshProgress(Guid id) @@ -986,10 +980,7 @@ namespace MediaBrowser.Providers.Manager { _activeRefreshes[id] = progress; - if (RefreshProgress != null) - { - RefreshProgress(this, new GenericEventArgs>(new Tuple(item, progress))); - } + RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); } else { @@ -1079,17 +1070,14 @@ namespace MediaBrowser.Providers.Manager await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); // Collection folders don't validate their children so we'll have to simulate that here - var collectionFolder = item as CollectionFolder; - if (collectionFolder != null) + if (item is CollectionFolder collectionFolder) { await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); } else { - var folder = item as Folder; - - if (folder != null) + if (item is Folder folder) { await folder.ValidateChildren(new SimpleProgress(), cancellationToken, options).ConfigureAwait(false); } @@ -1098,16 +1086,11 @@ namespace MediaBrowser.Providers.Manager private async Task RefreshCollectionFolderChildren(MetadataRefreshOptions options, CollectionFolder collectionFolder, CancellationToken cancellationToken) { - foreach (var child in collectionFolder.GetPhysicalFolders().ToList()) + foreach (var child in collectionFolder.GetPhysicalFolders()) { await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - if (child.IsFolder) - { - var folder = (Folder)child; - - await folder.ValidateChildren(new SimpleProgress(), cancellationToken, options, true).ConfigureAwait(false); - } + await child.ValidateChildren(new SimpleProgress(), cancellationToken, options, true).ConfigureAwait(false); } } @@ -1116,20 +1099,18 @@ namespace MediaBrowser.Providers.Manager var albums = _libraryManagerFactory() .GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + IncludeItemTypes = new[] { nameof(MusicAlbum) }, ArtistIds = new[] { item.Id }, DtoOptions = new DtoOptions(false) { EnableImages = false } }) - .OfType() - .ToList(); + .OfType(); var musicArtists = albums .Select(i => i.MusicArtist) - .Where(i => i != null) - .ToList(); + .Where(i => i != null); var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress(), cancellationToken, options, true)); diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index aee9921034..f86cdeab9f 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Music var releaseId = searchInfo.GetReleaseId(); var releaseGroupId = searchInfo.GetReleaseGroupId(); - string url = null; + string url; var isNameSearch = false; bool forceMusicBrainzProper = false; @@ -100,10 +100,10 @@ namespace MediaBrowser.Providers.Music } } - return new List(); + return Enumerable.Empty(); } - private List GetResultsFromResponse(Stream stream) + private IEnumerable GetResultsFromResponse(Stream stream) { using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -149,7 +149,7 @@ namespace MediaBrowser.Providers.Music return result; - }).ToList(); + }); } } } @@ -301,7 +301,7 @@ namespace MediaBrowser.Providers.Music public List> Artists = new List>(); - public static List Parse(XmlReader reader) + public static IEnumerable Parse(XmlReader reader) { reader.MoveToContent(); reader.Read(); @@ -338,13 +338,11 @@ namespace MediaBrowser.Providers.Music } } - return new List(); + return Enumerable.Empty(); } - private static List ParseReleaseList(XmlReader reader) + private static IEnumerable ParseReleaseList(XmlReader reader) { - var list = new List(); - reader.MoveToContent(); reader.Read(); @@ -369,7 +367,7 @@ namespace MediaBrowser.Providers.Music var release = ParseRelease(subReader, releaseId); if (release != null) { - list.Add(release); + yield return release; } } break; @@ -386,8 +384,6 @@ namespace MediaBrowser.Providers.Music reader.Read(); } } - - return list; } private static ReleaseResult ParseRelease(XmlReader reader, string releaseId) @@ -552,7 +548,7 @@ namespace MediaBrowser.Providers.Music return (null, null); } - private static ValueTuple ParseArtistArtistCredit(XmlReader reader, string artistId) + private static (string name, string id) ParseArtistArtistCredit(XmlReader reader, string artistId) { reader.MoveToContent(); reader.Read(); @@ -586,7 +582,7 @@ namespace MediaBrowser.Providers.Music } } - return new ValueTuple(name, artistId); + return (name, artistId); } private async Task GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs index 924b712507..55aab7778a 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Providers.Music { using (var stream = response.Content) { - var results = GetResultsFromResponse(stream); + var results = GetResultsFromResponse(stream).ToList(); if (results.Count > 0) { @@ -74,10 +74,10 @@ namespace MediaBrowser.Providers.Music } } - return new List(); + return Enumerable.Empty(); } - private List GetResultsFromResponse(Stream stream) + private IEnumerable GetResultsFromResponse(Stream stream) { using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -126,15 +126,13 @@ namespace MediaBrowser.Providers.Music } } - return new List(); + return Enumerable.Empty(); } } } - private List ParseArtistList(XmlReader reader) + private IEnumerable ParseArtistList(XmlReader reader) { - var list = new List(); - reader.MoveToContent(); reader.Read(); @@ -159,7 +157,7 @@ namespace MediaBrowser.Providers.Music var artist = ParseArtist(subReader, mbzId); if (artist != null) { - list.Add(artist); + yield return artist; } } break; @@ -176,8 +174,6 @@ namespace MediaBrowser.Providers.Music reader.Read(); } } - - return list; } private RemoteSearchResult ParseArtist(XmlReader reader, string artistId) @@ -277,7 +273,7 @@ namespace MediaBrowser.Providers.Music /// /// The name. /// System.String. - private string UrlEncode(string name) + private static string UrlEncode(string name) { return WebUtility.UrlEncode(name); } From 6d3e6d800fd8c6d5cb22271e2177946ab286855f Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Wed, 13 Mar 2019 20:31:21 +0000 Subject: [PATCH 0216/6208] Only delay making request if necessary When requesting data from MusicBrainz, only delay the request if previous request was less than rate limit ago, instead of always delaying for one second at start. --- .../Music/MusicBrainzAlbumProvider.cs | 62 +++++++------------ .../Music/MusicBrainzArtistProvider.cs | 6 +- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index f86cdeab9f..8a3860d74e 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -28,6 +29,7 @@ namespace MediaBrowser.Providers.Music private readonly IApplicationHost _appHost; private readonly ILogger _logger; private readonly IJsonSerializer _json; + private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); public readonly string MusicBrainzBaseUrl; @@ -45,6 +47,9 @@ namespace MediaBrowser.Providers.Music MusicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; + // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit + _stopWatchMusicBrainz.Start(); + Current = this; } @@ -54,8 +59,6 @@ namespace MediaBrowser.Providers.Music var releaseGroupId = searchInfo.GetReleaseGroupId(); string url; - var isNameSearch = false; - bool forceMusicBrainzProper = false; if (!string.IsNullOrEmpty(releaseId)) { @@ -64,7 +67,6 @@ namespace MediaBrowser.Providers.Music else if (!string.IsNullOrEmpty(releaseGroupId)) { url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); - forceMusicBrainzProper = true; } else { @@ -78,8 +80,6 @@ namespace MediaBrowser.Providers.Music } else { - isNameSearch = true; - // I'm sure there is a better way but for now it resolves search for 12" Mixes var queryName = searchInfo.Name.Replace("\"", string.Empty); @@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(url)) { - using (var response = await GetMusicBrainzResponse(url, isNameSearch, forceMusicBrainzProper, cancellationToken).ConfigureAwait(false)) + using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) { using (var stream = response.Content) { @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Music WebUtility.UrlEncode(albumName), artistId); - using (var response = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -272,7 +272,7 @@ namespace MediaBrowser.Providers.Music WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(artistName)); - using (var response = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -589,7 +589,7 @@ namespace MediaBrowser.Providers.Music { var url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); - using (var response = await GetMusicBrainzResponse(url, true, true, cancellationToken).ConfigureAwait(false)) + using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -625,7 +625,7 @@ namespace MediaBrowser.Providers.Music { var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId); - using (var response = await GetMusicBrainzResponse(url, false, cancellationToken).ConfigureAwait(false)) + using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) using (var oReader = new StreamReader(stream, Encoding.UTF8)) { @@ -710,34 +710,32 @@ namespace MediaBrowser.Providers.Music return null; } - internal Task GetMusicBrainzResponse(string url, bool isSearch, CancellationToken cancellationToken) - { - return GetMusicBrainzResponse(url, isSearch, false, cancellationToken); - } - /// - /// Gets the music brainz response. + /// Makes request to MusicBrainz server and awaits a response. /// - internal async Task GetMusicBrainzResponse(string url, bool isSearch, bool forceMusicBrainzProper, CancellationToken cancellationToken) + internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - var urlInfo = new MbzUrl(MusicBrainzBaseUrl, 1000); - var throttleMs = urlInfo.throttleMs; + // The Jellyfin user-agent is unrestricted but source IP must not exceed + // one request per second, therefore we rate limit to avoid throttling + // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting + const long QueryIntervalMs = 1000u; - if (throttleMs > 0) + // Only delay if necessary + if (_stopWatchMusicBrainz.ElapsedMilliseconds < QueryIntervalMs) { - // MusicBrainz is extremely adamant about limiting to one request per second - _logger.LogDebug("Throttling MusicBrainz by {0}ms", throttleMs.ToString(CultureInfo.InvariantCulture)); - await Task.Delay(throttleMs, cancellationToken).ConfigureAwait(false); + var delayMs = QueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); } - url = urlInfo.url.TrimEnd('/') + url; + _logger.LogDebug("MusicBrainz time since previous request: {0}ms", _stopWatchMusicBrainz.ElapsedMilliseconds); + _stopWatchMusicBrainz.Restart(); var options = new HttpRequestOptions { - Url = url, + Url = MusicBrainzBaseUrl.TrimEnd('/') + url, CancellationToken = cancellationToken, UserAgent = _appHost.ApplicationUserAgent, - BufferContent = throttleMs > 0 + BufferContent = false }; return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); @@ -749,17 +747,5 @@ namespace MediaBrowser.Providers.Music { throw new NotImplementedException(); } - - internal class MbzUrl - { - internal MbzUrl(string url, int throttleMs) - { - this.url = url; - this.throttleMs = throttleMs; - } - - public string url { get; set; } - public int throttleMs { get; set; } - } } } diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs index 55aab7778a..59280df897 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Music { var url = string.Format("/ws/2/artist/?query=arid:{0}", musicBrainzId); - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, false, cancellationToken).ConfigureAwait(false)) + using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) { using (var stream = response.Content) { @@ -46,7 +46,7 @@ namespace MediaBrowser.Providers.Music var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) { using (var stream = response.Content) { @@ -64,7 +64,7 @@ namespace MediaBrowser.Providers.Music // Try again using the search with accent characters url url = string.Format("/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false)) + using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) { using (var stream = response.Content) { From f8bb7a7ff4d87d19a43f97fed557aa92a657a50f Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Thu, 14 Mar 2019 00:43:41 +0000 Subject: [PATCH 0217/6208] Increased interval to 1050ms and moved to class scope Review comments from JustAMan. --- .../Music/MusicBrainzAlbumProvider.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 8a3860d74e..a1366e0fde 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -33,6 +33,12 @@ namespace MediaBrowser.Providers.Music public readonly string MusicBrainzBaseUrl; + // The Jellyfin user-agent is unrestricted but source IP must not exceed + // one request per second, therefore we rate limit to avoid throttling. + // Be prudent, use a value slightly above the minimun required. + // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting + private const long MusicBrainzQueryIntervalMs = 1050u; + public MusicBrainzAlbumProvider( IHttpClient httpClient, IApplicationHost appHost, @@ -715,15 +721,10 @@ namespace MediaBrowser.Providers.Music /// internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - // The Jellyfin user-agent is unrestricted but source IP must not exceed - // one request per second, therefore we rate limit to avoid throttling - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting - const long QueryIntervalMs = 1000u; - - // Only delay if necessary - if (_stopWatchMusicBrainz.ElapsedMilliseconds < QueryIntervalMs) + if (_stopWatchMusicBrainz.ElapsedMilliseconds < MusicBrainzQueryIntervalMs) { - var delayMs = QueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + // MusicBrainz is extremely adamant about limiting to one request per second + var delayMs = MusicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); } From ee7bf86e0f76cdd655f9f9766a4b99f5210ce985 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Thu, 14 Mar 2019 22:05:33 +0100 Subject: [PATCH 0218/6208] Adjusted the Product Name so the User Agent is correct/better. --- BDInfo/Properties/AssemblyInfo.cs | 2 +- DvdLib/Properties/AssemblyInfo.cs | 2 +- Emby.Dlna/Properties/AssemblyInfo.cs | 2 +- Emby.Drawing/Properties/AssemblyInfo.cs | 2 +- Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs | 2 +- Emby.Naming/Properties/AssemblyInfo.cs | 2 +- Emby.Notifications/Properties/AssemblyInfo.cs | 2 +- Emby.Photos/Properties/AssemblyInfo.cs | 2 +- Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs | 2 +- Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs | 2 +- Jellyfin.Server/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Api/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Common/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Controller/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Model/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Providers/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Tests/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs | 2 +- Mono.Nat/Properties/AssemblyInfo.cs | 2 +- RSSDP/Properties/AssemblyInfo.cs | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/BDInfo/Properties/AssemblyInfo.cs b/BDInfo/Properties/AssemblyInfo.cs index 788cf73666..482cd376a1 100644 --- a/BDInfo/Properties/AssemblyInfo.cs +++ b/BDInfo/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2016 CinemaSquid. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs index 5fc055d1f0..09d1a3801a 100644 --- a/DvdLib/Properties/AssemblyInfo.cs +++ b/DvdLib/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs index 9d3a22c970..0e3b03a322 100644 --- a/Emby.Dlna/Properties/AssemblyInfo.cs +++ b/Emby.Dlna/Properties/AssemblyInfo.cs @@ -8,7 +8,7 @@ using System.Resources; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.Drawing/Properties/AssemblyInfo.cs b/Emby.Drawing/Properties/AssemblyInfo.cs index 8dfefe0af8..4429839606 100644 --- a/Emby.Drawing/Properties/AssemblyInfo.cs +++ b/Emby.Drawing/Properties/AssemblyInfo.cs @@ -8,7 +8,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs b/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs index d60eccc2ee..702aed3b5a 100644 --- a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs +++ b/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.Naming/Properties/AssemblyInfo.cs b/Emby.Naming/Properties/AssemblyInfo.cs index 15311570b8..9295a1ec75 100644 --- a/Emby.Naming/Properties/AssemblyInfo.cs +++ b/Emby.Naming/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.Notifications/Properties/AssemblyInfo.cs b/Emby.Notifications/Properties/AssemblyInfo.cs index fd70375515..53b80e3dc1 100644 --- a/Emby.Notifications/Properties/AssemblyInfo.cs +++ b/Emby.Notifications/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.Photos/Properties/AssemblyInfo.cs b/Emby.Photos/Properties/AssemblyInfo.cs index 262125d388..c3ebc4e08e 100644 --- a/Emby.Photos/Properties/AssemblyInfo.cs +++ b/Emby.Photos/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs b/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs index ff2efb0781..69a532c357 100644 --- a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs +++ b/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs b/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs index ea1c457f6c..9fdb9529f5 100644 --- a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs +++ b/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs index 2959cdf1fe..1add32c8c1 100644 --- a/Jellyfin.Server/Properties/AssemblyInfo.cs +++ b/Jellyfin.Server/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs index f867230311..fb7d43f456 100644 --- a/MediaBrowser.Api/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Api/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs index 1a8fdb618d..d428cea2fd 100644 --- a/MediaBrowser.Common/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Common/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs index 007a1d739b..0841e84f24 100644 --- a/MediaBrowser.Controller/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs index 3eac837081..4d09084d20 100644 --- a/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs +++ b/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs index 6ecdf89bc1..8851ec5277 100644 --- a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs +++ b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs index e78719e35f..f118294817 100644 --- a/MediaBrowser.Model/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs index d2b13fc89d..eb2599e35a 100644 --- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.Tests/Properties/AssemblyInfo.cs b/MediaBrowser.Tests/Properties/AssemblyInfo.cs index 9bc2a19f21..1bd3ef5d64 100644 --- a/MediaBrowser.Tests/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Tests/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin System")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs index 416ce4826a..346853f588 100644 --- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs +++ b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs index 11e513f1be..343befb7e2 100644 --- a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs +++ b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Mono.Nat/Properties/AssemblyInfo.cs b/Mono.Nat/Properties/AssemblyInfo.cs index ea1c9dee6d..c24a4fc6c2 100644 --- a/Mono.Nat/Properties/AssemblyInfo.cs +++ b/Mono.Nat/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2006 Alan McGovern. Copyright © 2007 Ben Motmans. Code releases unde the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/RSSDP/Properties/AssemblyInfo.cs b/RSSDP/Properties/AssemblyInfo.cs index d2bb7c6f31..690ddb807c 100644 --- a/RSSDP/Properties/AssemblyInfo.cs +++ b/RSSDP/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] +[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] From 21cc38fcf426f5ecaedbe8a94007a89663de749c Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Thu, 14 Mar 2019 22:17:56 +0100 Subject: [PATCH 0219/6208] Adjusted AssemblyCopyright attribute values. --- BDInfo/Properties/AssemblyInfo.cs | 2 +- DvdLib/Properties/AssemblyInfo.cs | 2 +- Emby.Dlna/Properties/AssemblyInfo.cs | 2 +- Emby.Drawing/Properties/AssemblyInfo.cs | 2 +- Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs | 2 +- Emby.Naming/Properties/AssemblyInfo.cs | 2 +- Emby.Notifications/Properties/AssemblyInfo.cs | 2 +- Emby.Photos/Properties/AssemblyInfo.cs | 2 +- Emby.Server.Implementations/Properties/AssemblyInfo.cs | 2 +- Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs | 2 +- Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs | 2 +- Jellyfin.Server/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Api/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Common/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Controller/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Model/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.Providers/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs | 2 +- Mono.Nat/Properties/AssemblyInfo.cs | 2 +- RSSDP/Properties/AssemblyInfo.cs | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/BDInfo/Properties/AssemblyInfo.cs b/BDInfo/Properties/AssemblyInfo.cs index 482cd376a1..f65c7036a4 100644 --- a/BDInfo/Properties/AssemblyInfo.cs +++ b/BDInfo/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2016 CinemaSquid. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2016 CinemaSquid. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs index 09d1a3801a..6acd571d68 100644 --- a/DvdLib/Properties/AssemblyInfo.cs +++ b/DvdLib/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs index 0e3b03a322..a2c1e0db8b 100644 --- a/Emby.Dlna/Properties/AssemblyInfo.cs +++ b/Emby.Dlna/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Resources; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Drawing/Properties/AssemblyInfo.cs b/Emby.Drawing/Properties/AssemblyInfo.cs index 4429839606..281008e370 100644 --- a/Emby.Drawing/Properties/AssemblyInfo.cs +++ b/Emby.Drawing/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs b/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs index 702aed3b5a..5956fc3b31 100644 --- a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs +++ b/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Naming/Properties/AssemblyInfo.cs b/Emby.Naming/Properties/AssemblyInfo.cs index 9295a1ec75..f26e0ba794 100644 --- a/Emby.Naming/Properties/AssemblyInfo.cs +++ b/Emby.Naming/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Notifications/Properties/AssemblyInfo.cs b/Emby.Notifications/Properties/AssemblyInfo.cs index 53b80e3dc1..5c82c90c47 100644 --- a/Emby.Notifications/Properties/AssemblyInfo.cs +++ b/Emby.Notifications/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Photos/Properties/AssemblyInfo.cs b/Emby.Photos/Properties/AssemblyInfo.cs index c3ebc4e08e..a3bb4362a9 100644 --- a/Emby.Photos/Properties/AssemblyInfo.cs +++ b/Emby.Photos/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.Server.Implementations/Properties/AssemblyInfo.cs b/Emby.Server.Implementations/Properties/AssemblyInfo.cs index 79ba9374ce..a1933f66ef 100644 --- a/Emby.Server.Implementations/Properties/AssemblyInfo.cs +++ b/Emby.Server.Implementations/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs b/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs index 69a532c357..7beec09cbd 100644 --- a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs +++ b/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs b/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs index 9fdb9529f5..e7db09449d 100644 --- a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs +++ b/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs index 1add32c8c1..5de1e653d9 100644 --- a/Jellyfin.Server/Properties/AssemblyInfo.cs +++ b/Jellyfin.Server/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs index fb7d43f456..35bcbea5cd 100644 --- a/MediaBrowser.Api/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Api/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs index d428cea2fd..538e89fd1c 100644 --- a/MediaBrowser.Common/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Common/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs index 0841e84f24..60e7923091 100644 --- a/MediaBrowser.Controller/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs index 4d09084d20..580cef9da6 100644 --- a/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs +++ b/MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs index 8851ec5277..a9491374bd 100644 --- a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs +++ b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs index f118294817..f99e9ece96 100644 --- a/MediaBrowser.Model/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs index eb2599e35a..f1c46899ce 100644 --- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs index 346853f588..584d490216 100644 --- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs +++ b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs index 343befb7e2..b3e2f27179 100644 --- a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs +++ b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/Mono.Nat/Properties/AssemblyInfo.cs b/Mono.Nat/Properties/AssemblyInfo.cs index c24a4fc6c2..dc47f2ffeb 100644 --- a/Mono.Nat/Properties/AssemblyInfo.cs +++ b/Mono.Nat/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2006 Alan McGovern. Copyright © 2007 Ben Motmans. Code releases unde the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2006 Alan McGovern. Copyright © 2007 Ben Motmans. Code releases under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/RSSDP/Properties/AssemblyInfo.cs b/RSSDP/Properties/AssemblyInfo.cs index 690ddb807c..55f7b6a834 100644 --- a/RSSDP/Properties/AssemblyInfo.cs +++ b/RSSDP/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] From 427688a0a08e04e87774d01e7bfec22eecc71e7d Mon Sep 17 00:00:00 2001 From: redSpoutnik <15638041+redSpoutnik@users.noreply.github.com> Date: Thu, 14 Mar 2019 22:31:51 +0100 Subject: [PATCH 0220/6208] Change subtitles DisplayTitle behavior --- .../ApplicationHost.cs | 5 ++-- .../Data/SqliteItemRepository.cs | 13 +++++++++- .../Encoder/MediaEncoder.cs | 8 ++++-- .../Probing/ProbeResultNormalizer.cs | 8 +++++- MediaBrowser.Model/Entities/MediaStream.cs | 26 +++++++++++++++---- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b3bb4f740e..f185d37adf 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -703,7 +703,7 @@ namespace Emby.Server.Implementations var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager); serviceCollection.AddSingleton(displayPreferencesRepo); - ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo); + ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo, LocalizationManager); serviceCollection.AddSingleton(ItemRepository); AuthenticationRepository = GetAuthenticationRepository(); @@ -979,7 +979,8 @@ namespace Emby.Server.Implementations HttpClient, ZipClient, ProcessFactory, - 5000); + 5000, + LocalizationManager); MediaEncoder = mediaEncoder; serviceCollection.AddSingleton(MediaEncoder); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 06f6563a32..1aeb3b9c5c 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Reflection; @@ -56,6 +57,8 @@ namespace Emby.Server.Implementations.Data private readonly IServerConfigurationManager _config; private IServerApplicationHost _appHost; + private readonly ILocalizationManager _localization; + public IImageProcessor ImageProcessor { get; set; } /// @@ -66,7 +69,8 @@ namespace Emby.Server.Implementations.Data IServerApplicationHost appHost, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, - IAssemblyInfo assemblyInfo) + IAssemblyInfo assemblyInfo, + ILocalizationManager localization) : base(loggerFactory.CreateLogger(nameof(SqliteItemRepository))) { if (config == null) @@ -83,6 +87,7 @@ namespace Emby.Server.Implementations.Data _config = config; _jsonSerializer = jsonSerializer; _typeMapper = new TypeMapper(assemblyInfo); + _localization = localization; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } @@ -6189,6 +6194,12 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type item.ColorTransfer = reader[34].ToString(); } + if (item.Type == MediaStreamType.Subtitle){ + item.localizedUndefined = _localization.GetLocalizedString("Undefined"); + item.localizedDefault = _localization.GetLocalizedString("Default"); + item.localizedForced = _localization.GetLocalizedString("Forced"); + } + return item; } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f29c06b4c..d7449d57d2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; @@ -69,6 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string _originalFFMpegPath; private readonly string _originalFFProbePath; private readonly int DefaultImageExtractionTimeoutMs; + private readonly ILocalizationManager _localization; public MediaEncoder( ILoggerFactory loggerFactory, @@ -88,7 +90,8 @@ namespace MediaBrowser.MediaEncoding.Encoder IHttpClient httpClient, IZipClient zipClient, IProcessFactory processFactory, - int defaultImageExtractionTimeoutMs) + int defaultImageExtractionTimeoutMs, + ILocalizationManager localization) { _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _jsonSerializer = jsonSerializer; @@ -110,6 +113,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _originalFFProbePath = ffProbePath; _originalFFMpegPath = ffMpegPath; _hasExternalEncoder = hasExternalEncoder; + _localization = localization; } public string EncoderLocationType @@ -537,7 +541,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, FileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } catch { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 5099ccb2aa..3e6e5864cd 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -20,11 +21,13 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; - public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem) + public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, ILocalizationManager localization) { _logger = logger; _fileSystem = fileSystem; + _localization = localization; } public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) @@ -599,6 +602,9 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.Type = MediaStreamType.Subtitle; stream.Codec = NormalizeSubtitleCodec(stream.Codec); + stream.localizedUndefined = _localization.GetLocalizedString("Undefined"); + stream.localizedDefault = _localization.GetLocalizedString("Default"); + stream.localizedForced = _localization.GetLocalizedString("Forced"); } else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index fc346df376..8dcf679a3d 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Text; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; @@ -65,6 +67,10 @@ namespace MediaBrowser.Model.Entities } } + public string localizedUndefined { get; set; } + public string localizedDefault { get; set; } + public string localizedForced { get; set; } + public string DisplayTitle { get @@ -141,22 +147,30 @@ namespace MediaBrowser.Model.Entities } else { - attributes.Add("Und"); + attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); } if (IsDefault) { - attributes.Add("Default"); + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); } if (IsForced) { - attributes.Add("Forced"); + attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); } - string name = string.Join(" ", attributes.ToArray()); + if (!string.IsNullOrEmpty(Title)) + { + return attributes.AsEnumerable() + // keep Tags that are not already in Title + .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + // attributes concatenation, starting with Title + .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) + .ToString(); + } - return name; + return string.Join(" - ", attributes.ToArray()); } if (Type == MediaStreamType.Video) @@ -220,6 +234,7 @@ namespace MediaBrowser.Model.Entities return null; } + /* private string AddLanguageIfNeeded(string title) { if (!string.IsNullOrEmpty(Language) && @@ -241,6 +256,7 @@ namespace MediaBrowser.Model.Entities return false; } + */ public string NalLengthSize { get; set; } From d125fbc43d453be7de08375dede1748d4c19a8cf Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Thu, 14 Mar 2019 21:32:27 +0000 Subject: [PATCH 0221/6208] Added contact email to user agent MusicBrainz request a contact email address is supplied in comment section of user agent field. --- Emby.Server.Implementations/ApplicationHost.cs | 6 ++++++ MediaBrowser.Common/IApplicationHost.cs | 6 ++++++ MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs | 5 +++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 94eaffa479..eaea8844d4 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -428,6 +428,12 @@ namespace Emby.Server.Implementations /// The application user agent. public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion; + /// + /// Gets the email address for use within a comment section of a user agent field. + /// Presently used to provide contact information to MusicBrainz service. + /// + public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org"; + private string _productName; /// diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 966448d630..2925a3efd9 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -71,6 +71,12 @@ namespace MediaBrowser.Common /// The application user agent. string ApplicationUserAgent { get; } + /// + /// Gets the email address for use within a comment section of a user agent field. + /// Presently used to provide contact information to MusicBrainz service. + /// + string ApplicationUserAgentAddress { get; } + /// /// Gets the exports. /// diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index a1366e0fde..b716f40f04 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -735,7 +734,9 @@ namespace MediaBrowser.Providers.Music { Url = MusicBrainzBaseUrl.TrimEnd('/') + url, CancellationToken = cancellationToken, - UserAgent = _appHost.ApplicationUserAgent, + // MusicBrainz request a contact email address is supplied, as comment, in user agent field: + // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent + UserAgent = string.Format("{0} ( {1} )", _appHost.ApplicationUserAgent, _appHost.ApplicationUserAgentAddress), BufferContent = false }; From dd929d796f10d06f5874909d7572fa49e750fb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Fri, 15 Mar 2019 14:30:15 +0100 Subject: [PATCH 0222/6208] Only remove /var/lib/apt/lists/* --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 72c0cdbf3a..5794bdde1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update \ libfontconfig1 \ && apt-get clean autoclean \ && apt-get autoremove \ - && rm -rf /var/lib/{apt,dpkg,cache,log} \ + && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=ffmpeg / / From 31305af7ffafabaf88911b67665751f74903199e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Fri, 15 Mar 2019 14:37:11 +0100 Subject: [PATCH 0223/6208] Clean apt lists in arm Dockerfiles --- Dockerfile.arm | 1 + Dockerfile.arm64 | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile.arm b/Dockerfile.arm index 238e90ccd8..1497da0ef7 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -25,6 +25,7 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 COPY --from=qemu_extract qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ + && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 2927d4e658..f4658a055c 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -26,6 +26,7 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 COPY --from=qemu_extract qemu-aarch64-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ + && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin From 764c901cd7adc5ae95d2657822d2910e0c2e8468 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 15 Mar 2019 17:34:15 +0100 Subject: [PATCH 0224/6208] Fix exception caused by #1096 ```cs MediaBrowser.Common.Extensions.ResourceNotFoundException: Configuration with key subtitles not found. at Emby.Server.Implementations.AppBase.BaseConfigurationManager.<>c__DisplayClass42_0.b__0(String k) in /home/pi/dev/jellyfin/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs:line 247 at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Emby.Server.Implementations.AppBase.BaseConfigurationManager.GetConfiguration(String key) in /home/pi/dev/jellyfin/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs:line 238 at MediaBrowser.Providers.MediaInfo.FFProbeVideoInfo.AddExternalSubtitles(Video video, List`1 currentStreams, MetadataRefreshOptions options, CancellationToken cancellationToken) in /home/pi/dev/jellyfin/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs:line 486 at MediaBrowser.Providers.MediaInfo.FFProbeVideoInfo.Fetch(Video video, CancellationToken cancellationToken, MediaInfo mediaInfo, BlurayDiscInfo blurayInfo, MetadataRefreshOptions options) in /home/pi/dev/jellyfin/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs:line 204 at MediaBrowser.Providers.MediaInfo.FFProbeVideoInfo.ProbeVideo[T](T item, MetadataRefreshOptions options, CancellationToken cancellationToken) in /home/pi/dev/jellyfin/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs:line 119 at MediaBrowser.Providers.Manager.MetadataService`2.RunCustomProvider(ICustomMetadataProvider`1 provider, TItemType item, String logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken) in /home/pi/dev/jellyfin/MediaBrowser.Providers/Manager/MetadataService.cs:line 806 ``` --- .../Providers/SubtitleConfigurationFactory.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs new file mode 100644 index 0000000000..09d974db6c --- /dev/null +++ b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Common.Providers +{ + public class SubtitleConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + yield return new ConfigurationStore() + { + Key = "subtitles", + ConfigurationType = typeof(SubtitleOptions) + }; + } + } +} From d5f080fefbac63736a8c01acebe08e6617c5c486 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 15 Mar 2019 17:56:19 +0100 Subject: [PATCH 0225/6208] Check before flushing ffmpeg log The stream could have been diposed while writing. --- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 46593fb2fc..2755bf5814 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.MediaEncoding { public class JobLogger { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private readonly ILogger _logger; public JobLogger(ILogger logger) @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding { using (var reader = new StreamReader(source)) { - while (!reader.EndOfStream) + while (!reader.EndOfStream && reader.BaseStream.CanRead) { var line = await reader.ReadLineAsync().ConfigureAwait(false); @@ -39,6 +39,13 @@ namespace MediaBrowser.Controller.MediaEncoding } await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + + // Check again, the stream could have been closed + if (!target.CanWrite) + { + break; + } + await target.FlushAsync().ConfigureAwait(false); } } From d2e408539ea32eaad573ff1deb6f77a2d80a70d3 Mon Sep 17 00:00:00 2001 From: PloughPuff Date: Fri, 15 Mar 2019 19:29:04 +0000 Subject: [PATCH 0226/6208] MusicBrainz 503 Retry Strategy Upon receiving back a 503 Service Unavailable from MusicBrainz (indicating throttling), retry the same request a number of times before giving up. --- .../Music/MusicBrainzAlbumProvider.cs | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index b716f40f04..3797f9039a 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -32,12 +32,21 @@ namespace MediaBrowser.Providers.Music public readonly string MusicBrainzBaseUrl; - // The Jellyfin user-agent is unrestricted but source IP must not exceed - // one request per second, therefore we rate limit to avoid throttling. - // Be prudent, use a value slightly above the minimun required. - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting + /// + /// The Jellyfin user-agent is unrestricted but source IP must not exceed + /// one request per second, therefore we rate limit to avoid throttling. + /// Be prudent, use a value slightly above the minimun required. + /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting + /// private const long MusicBrainzQueryIntervalMs = 1050u; + /// + /// For each single MB lookup/search, this is the maximum number of + /// attempts that shall be made whilst receiving a 503 Server + /// Unavailable (indicating throttled) response. + /// + private const uint MusicBrainzQueryAttempts = 5u; + public MusicBrainzAlbumProvider( IHttpClient httpClient, IApplicationHost appHost, @@ -717,19 +726,12 @@ namespace MediaBrowser.Providers.Music /// /// Makes request to MusicBrainz server and awaits a response. + /// A 503 Service Unavailable response indicates throttling to maintain a rate limit. + /// A number of retries shall be made in order to try and satisfy the request before + /// giving up and returning null. /// internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - if (_stopWatchMusicBrainz.ElapsedMilliseconds < MusicBrainzQueryIntervalMs) - { - // MusicBrainz is extremely adamant about limiting to one request per second - var delayMs = MusicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; - await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); - } - - _logger.LogDebug("MusicBrainz time since previous request: {0}ms", _stopWatchMusicBrainz.ElapsedMilliseconds); - _stopWatchMusicBrainz.Restart(); - var options = new HttpRequestOptions { Url = MusicBrainzBaseUrl.TrimEnd('/') + url, @@ -740,7 +742,38 @@ namespace MediaBrowser.Providers.Music BufferContent = false }; - return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + HttpResponseInfo response; + var attempts = 0u; + + do + { + attempts++; + + if (_stopWatchMusicBrainz.ElapsedMilliseconds < MusicBrainzQueryIntervalMs) + { + // MusicBrainz is extremely adamant about limiting to one request per second + var delayMs = MusicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); + } + + // Write time since last request to debug log as evidence we're meeting rate limit + // requirement, before resetting stopwatch back to zero. + _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); + _stopWatchMusicBrainz.Restart(); + + response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + + // We retry a finite number of times, and only whilst MB is indcating 503 (throttling) + } + while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + + // Log error if unable to query MB database due to throttling + if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable ) + { + _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.Url); + } + + return response; } public int Order => 0; From 221389089cc9ca4b69907d6bf3e9d38bf94393ea Mon Sep 17 00:00:00 2001 From: Phallacy Date: Fri, 15 Mar 2019 21:25:19 -0700 Subject: [PATCH 0227/6208] quick fix for auth bug --- .../Library/DefaultAuthenticationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 3ec1f81d31..3d15a8afbb 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library PasswordHash readyHash = new PasswordHash(resolvedUser.Password); byte[] calculatedHash; string calculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { if (string.IsNullOrEmpty(readyHash.Salt)) { From 1ee016c99745ed4a29f8995de1478ab6a6e410e9 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 00:18:52 -0700 Subject: [PATCH 0228/6208] configurable user lockout --- Emby.Server.Implementations/Library/UserManager.cs | 14 +++++++++++--- MediaBrowser.Model/Users/UserPolicy.cs | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index efb1ef4a50..e20af003d4 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -219,7 +219,7 @@ 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), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, "^[\\w-'._@]*$"); + return Regex.IsMatch(username, @"^[\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) @@ -448,11 +448,19 @@ namespace Emby.Server.Implementations.Library user.Policy.InvalidLoginAttemptCount = newValue; - var maxCount = user.Policy.IsAdministrator ? 3 : 5; + // 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; - if (newValue >= maxCount) + // -1 can be used to specify no lockout value + if (maxCount != -1 && newValue >= maxCount) { _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); user.Policy.IsDisabled = true; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 27ce23778e..5415fd5e81 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Users public bool EnableAllFolders { get; set; } public int InvalidLoginAttemptCount { get; set; } + public int? LoginAttemptsBeforeLockout { get; set; } public bool EnablePublicSharing { get; set; } @@ -104,6 +105,8 @@ namespace MediaBrowser.Model.Users AccessSchedules = Array.Empty(); + LoginAttemptsBeforeLockout = -1; + EnableAllChannels = true; EnabledChannels = Array.Empty(); From 7f0fa74467d5f3560addd6cfefbfb2dbd24f4060 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 00:38:31 -0700 Subject: [PATCH 0229/6208] updated regex to string literal with escaped - --- Emby.Server.Implementations/Library/UserManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index efb1ef4a50..7e2419b680 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -216,10 +216,10 @@ namespace Emby.Server.Implementations.Library public static bool IsValidUsername(string username) { - //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 + // 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), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, "^[\\w-'._@]*$"); + return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) From 4a30fee40df69be381c2cdea01cd4dd29b97ad3c Mon Sep 17 00:00:00 2001 From: redSpoutnik <15638041+redSpoutnik@users.noreply.github.com> Date: Sat, 16 Mar 2019 17:28:45 +0100 Subject: [PATCH 0230/6208] Remove some dead code --- MediaBrowser.Model/Entities/MediaStream.cs | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 8dcf679a3d..5652962daf 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -134,10 +134,6 @@ namespace MediaBrowser.Model.Entities if (Type == MediaStreamType.Subtitle) { - //if (!string.IsNullOrEmpty(Title)) - //{ - // return AddLanguageIfNeeded(Title); - //} var attributes = new List(); @@ -234,30 +230,6 @@ namespace MediaBrowser.Model.Entities return null; } - /* - private string AddLanguageIfNeeded(string title) - { - if (!string.IsNullOrEmpty(Language) && - !string.Equals(Language, "und", StringComparison.OrdinalIgnoreCase) && - !IsLanguageInTitle(title, Language)) - { - title = StringHelper.FirstToUpper(Language) + " " + title; - } - - return title; - } - - private bool IsLanguageInTitle(string title, string language) - { - if (title.IndexOf(Language, StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return false; - } - */ - public string NalLengthSize { get; set; } /// From e3dbed1c1aff986e51e25ba244a6a6bf52816dbe Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 16 Mar 2019 10:16:23 -0700 Subject: [PATCH 0231/6208] Update Emby.Server.Implementations/Library/UserManager.cs Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 7e2419b680..ee90b3558e 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -218,7 +218,7 @@ 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), 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\-'._@]*$"); } From fc28c9237cf08bd0100629ad5830824397e52565 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 21:34:26 -0700 Subject: [PATCH 0232/6208] fixed line endings --- .../Library/DefaultPasswordResetProvider.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs new file mode 100644 index 0000000000..87dbe684cd --- /dev/null +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + public string Name => "Default Password Reset"; + + public bool IsEnabled => true; + + // set our default timeout to an hour since we'll be making the PIN it generates a little less fragile + public TimeSpan PasswordResetTimeout => new TimeSpan(1,0,0); + + public Task ResetPassword(User user) + { + throw new NotImplementedException(); + } + } +} From 80aedcd7e24a7708f44491e3cd359f911a124c8f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 21:36:45 -0700 Subject: [PATCH 0233/6208] really fixed line endings --- .../Library/DefaultPasswordResetProvider.cs | 22 - .../Library/UserManager.cs | 2438 ++++++++--------- 2 files changed, 1219 insertions(+), 1241 deletions(-) delete mode 100644 Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs deleted file mode 100644 index 87dbe684cd..0000000000 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - public string Name => "Default Password Reset"; - - public bool IsEnabled => true; - - // set our default timeout to an hour since we'll be making the PIN it generates a little less fragile - public TimeSpan PasswordResetTimeout => new TimeSpan(1,0,0); - - public Task ResetPassword(User user) - { - throw new NotImplementedException(); - } - } -} diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ce8c016602..4cf703add5 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,1219 +1,1219 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -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; -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; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Library -{ - /// - /// Class UserManager - /// - 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; } - - /// - /// Gets the active user repository - /// - /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - - private readonly INetworkManager _networkManager; - - private readonly Func _imageProcessorFactory; - private readonly Func _dtoServiceFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - - public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, - IUserRepository userRepository, - IXmlSerializer xmlSerializer, - INetworkManager networkManager, - Func imageProcessorFactory, - Func dtoServiceFactory, - IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - IFileSystem fileSystem) - { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty(); - - DeletePinFile(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(i => i.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = GetAuthenticationProviderId(i) - }) - .ToArray(); - } - - public void AddParts(IEnumerable authenticationProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.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 - /// - /// The id. - /// User. - /// - public User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException(nameof(id), "Guid can't be empty"); - } - - return Users.FirstOrDefault(u => u.Id == id); - } - - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. - public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } - - public User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - public void Initialize() - { - _users = LoadUsers(); - - var users = Users.ToList(); - - // If there are no local users with admin rights, make them all admins - if (!users.Any(i => i.Policy.IsAdministrator)) - { - foreach (var user in users) - { - user.Policy.IsAdministrator = true; - UpdateUserPolicy(user, user.Policy, false); - } - } - } - - public static bool IsValidUsername(string username) - { - // 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 (.) - return Regex.IsMatch(username, @"^[\w\-'._@]*$"); - } - - private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } - - public string MakeValidUsername(string username) - { - if (IsValidUsername(username)) - { - return username; - } - - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - var builder = new StringBuilder(); - - foreach (var c in username) - { - if (IsValidUsernameCharacter(c)) - { - builder.Append(c); - } - } - return builder.ToString(); - } - - public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - - var success = false; - IAuthenticationProvider authenticationProvider = null; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - } - else - { - // user is null - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) - { - user = await CreateUser(username).ConfigureAwait(false); - - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) - { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = GetAuthenticationProviderId(authenticationProvider); - - if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.Policy.AuthenticationProviderId = providerId; - UpdateUserPolicy(user, user.Policy, true); - } - } - - if (user == null) - { - throw new SecurityException("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)); - } - - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - UpdateInvalidLoginAttemptCount(user, 0); - } - else - { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); - } - - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - - return success ? user : null; - } - - private static string GetAuthenticationProviderId(IAuthenticationProvider provider) - { - return provider.GetType().FullName; - } - - private IAuthenticationProvider GetAuthenticationProvider(User user) - { - return GetAuthenticationProviders(user).First(); - } - - private IAuthenticationProvider[] GetAuthenticationProviders(User user) - { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - if (providers.Length == 0) - { - providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; - } - - return providers; - } - - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) - { - try - { - var requiresResolvedUser = provider as IRequiresResolvedUser; - if (requiresResolvedUser != null) - { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - await provider.Authenticate(username, password).ConfigureAwait(false); - } - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - - return false; - } - } - - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - if (password != null && user != null) - { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } - - if (password == null) - { - // legacy - success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) - { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - - if (success) - { - authenticationProvider = provider; - break; - } - } - } - - if (user != null) - { - 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); - } - } - } - - return new Tuple(authenticationProvider, success); - } - - private void UpdateInvalidLoginAttemptCount(User user, int newValue) - { - 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; - - // -1 can be used to specify no lockout value - if (maxCount != -1 && newValue >= maxCount) - { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); - user.Policy.IsDisabled = true; - - fireLockout = true; - } - - UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } - } - - private string GetLocalPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : user.EasyPassword; - } - - /// - /// Loads the users from the repository - /// - /// IEnumerable{User}. - private User[] LoadUsers() - { - var users = UserRepository.RetrieveAllUsers(); - - // There always has to be at least one user. - if (users.Count == 0) - { - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - var name = MakeValidUsername(defaultName); - - var user = InstantiateNewUser(name); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - users.Add(user); - - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - } - - return users.ToArray(); - } - - public UserDto GetUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); - - bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? - hasConfiguredEasyPassword : - hasConfiguredPassword; - - UserDto dto = new UserDto - { - Id = user.Id, - Name = user.Name, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword, - HasConfiguredEasyPassword = hasConfiguredEasyPassword, - LastActivityDate = user.LastActivityDate, - LastLoginDate = user.LastLoginDate, - Configuration = user.Configuration, - ServerId = _appHost.SystemId, - Policy = user.Policy - }; - - if (!hasPassword && Users.Count() == 1) - { - dto.EnableAutoLogin = true; - } - - ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); - - if (image != null) - { - dto.PrimaryImageTag = GetImageCacheTag(user, image); - - try - { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); - } - catch (Exception ex) - { - // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); - } - } - - return dto; - } - - public UserDto GetOfflineUserDto(User user) - { - var dto = GetUserDto(user); - - dto.ServerName = _appHost.FriendlyName; - - return dto; - } - - private string GetImageCacheTag(BaseItem item, ItemImageInfo image) - { - try - { - return _imageProcessorFactory().GetImageCacheTag(item, image); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); - return null; - } - } - - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - public async Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - foreach (var user in Users) - { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Renames the user. - /// - /// The user. - /// The new name. - /// Task. - /// user - /// - public async Task RenameUser(User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); - } - - if (user.Name.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - await user.Rename(newName); - - OnUserUpdated(user); - } - - /// - /// Updates the user. - /// - /// The user. - /// user - /// - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) - { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); - } - - user.DateModified = DateTime.UtcNow; - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.UpdateUser(user); - - OnUserUpdated(user); - } - - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - - /// - /// Creates the user. - /// - /// The name. - /// User. - /// name - /// - public async Task CreateUser(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!IsValidUsername(name)) - { - throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); - } - - if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - 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 list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - - return user; - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - /// - public async Task DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) - { - 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)); - } - - if (allUsers.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)); - } - - if (user.Policy.IsAdministrator && allUsers.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)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - 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); - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Resets the password by clearing it. - /// - /// Task. - public Task ResetPassword(User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (newPassword != null) - { - newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - /// - /// Instantiates the new user. - /// - /// The name. - /// User. - private static User InstantiateNewUser(string name) - { - return new User - { - Name = name, - Id = Guid.NewGuid(), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - //Salt = BCrypt.GenerateSalt() - }; - } - - private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); - - private string _lastPin; - private PasswordPinCreationResult _lastPasswordPinCreationResult; - private int _pinAttempts; - - private async Task CreatePasswordResetPin() - { - var num = new Random().Next(1, 9999); - - var path = PasswordResetFile; - - var pin = num.ToString("0000", CultureInfo.InvariantCulture); - _lastPin = pin; - - var time = TimeSpan.FromMinutes(5); - var expiration = DateTime.UtcNow.Add(time); - - var text = new StringBuilder(); - - var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; - - text.AppendLine("Use your web browser to visit:"); - text.AppendLine(string.Empty); - text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); - text.AppendLine(string.Empty); - text.AppendLine("Enter the following pin code:"); - text.AppendLine(string.Empty); - text.AppendLine(pin); - text.AppendLine(string.Empty); - - var localExpirationTime = expiration.ToLocalTime(); - // Tuesday, 22 August 2006 06:30 AM - text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); - - File.WriteAllText(path, text.ToString(), Encoding.UTF8); - - var result = new PasswordPinCreationResult - { - PinFile = path, - ExpirationDate = expiration - }; - - _lastPasswordPinCreationResult = result; - _pinAttempts = 0; - - return result; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - DeletePinFile(); - - var user = string.IsNullOrWhiteSpace(enteredUsername) ? - null : - GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - string pinFile = null; - DateTime? expirationDate = null; - - if (user != null && !user.Policy.IsAdministrator) - { - action = ForgotPasswordAction.ContactAdmin; - } - else - { - if (isInNetwork) - { - action = ForgotPasswordAction.PinCode; - } - - var result = await CreatePasswordResetPin().ConfigureAwait(false); - pinFile = result.PinFile; - expirationDate = result.ExpirationDate; - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = pinFile, - PinExpirationDate = expirationDate - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - DeletePinFile(); - - var usersReset = new List(); - - var valid = !string.IsNullOrWhiteSpace(_lastPin) && - string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && - _lastPasswordPinCreationResult != null && - _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; - - if (valid) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - - foreach (var user in Users) - { - await ResetPassword(user).ConfigureAwait(false); - - if (user.Policy.IsDisabled) - { - user.Policy.IsDisabled = false; - UpdateUserPolicy(user, user.Policy, true); - } - usersReset.Add(user.Name); - } - } - else - { - _pinAttempts++; - if (_pinAttempts >= 3) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - } - } - - return new PinRedeemResult - { - Success = valid, - UsersReset = usersReset.ToArray() - }; - } - - private void DeletePinFile() - { - try - { - _fileSystem.DeleteFile(PasswordResetFile); - } - catch - { - - } - } - - class PasswordPinCreationResult - { - public string PinFile { get; set; } - public DateTime ExpirationDate { get; set; } - } - - public UserPolicy GetUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - if (!File.Exists(path)) - { - return GetDefaultPolicy(user); - } - - try - { - lock (_policySyncLock) - { - return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); - } - } - catch (IOException) - { - return GetDefaultPolicy(user); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return GetDefaultPolicy(user); - } - } - - private static UserPolicy GetDefaultPolicy(User user) - { - return new UserPolicy - { - EnableContentDownloading = true, - EnableSyncTranscoding = true - }; - } - - private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) - { - var user = GetUserById(userId); - UpdateUserPolicy(user, userPolicy, true); - } - - private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) - { - // The xml serializer will output differently if the type is not exact - if (userPolicy.GetType() != typeof(UserPolicy)) - { - var json = _jsonSerializer.SerializeToString(userPolicy); - userPolicy = _jsonSerializer.DeserializeFromString(json); - } - - var path = GetPolicyFilePath(user); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_policySyncLock) - { - _xmlSerializer.SerializeToFile(userPolicy, path); - user.Policy = userPolicy; - } - - if (fireEvent) - { - UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - - private void DeleteUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - try - { - lock (_policySyncLock) - { - _fileSystem.DeleteFile(path); - } - } - catch (IOException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting policy file"); - } - } - - private static string GetPolicyFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); - } - - private static string GetConfigurationFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); - } - - public UserConfiguration GetUserConfiguration(User user) - { - var path = GetConfigurationFilePath(user); - - if (!File.Exists(path)) - { - return new UserConfiguration(); - } - - try - { - lock (_configSyncLock) - { - return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); - } - } - catch (IOException) - { - return new UserConfiguration(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return new UserConfiguration(); - } - } - - private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - UpdateConfiguration(user, config); - } - - public void UpdateConfiguration(User user, UserConfiguration config) - { - UpdateConfiguration(user, config, true); - } - - private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) - { - var path = GetConfigurationFilePath(user); - - // The xml serializer will output differently if the type is not exact - if (config.GetType() != typeof(UserConfiguration)) - { - var json = _jsonSerializer.SerializeToString(config); - config = _jsonSerializer.DeserializeFromString(json); - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - } - - public class DeviceAccessEntryPoint : IServerEntryPoint - { - private IUserManager _userManager; - private IAuthenticationRepository _authRepo; - private IDeviceManager _deviceManager; - private ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - - return Task.CompletedTask; - } - - private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.Policy.EnableAllDevices) - { - UpdateDeviceAccess(user); - } - } - - private void UpdateDeviceAccess(User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - - public void Dispose() - { - - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +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; +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; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Class UserManager + /// + 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; } + + /// + /// Gets the active user repository + /// + /// The user repository. + private IUserRepository UserRepository { get; set; } + public event EventHandler> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func _imageProcessorFactory; + private readonly Func _dtoServiceFactory; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + + public UserManager( + ILoggerFactory loggerFactory, + IServerConfigurationManager configurationManager, + IUserRepository userRepository, + IXmlSerializer xmlSerializer, + INetworkManager networkManager, + Func imageProcessorFactory, + Func dtoServiceFactory, + IServerApplicationHost appHost, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem) + { + _logger = loggerFactory.CreateLogger(nameof(UserManager)); + UserRepository = userRepository; + _xmlSerializer = xmlSerializer; + _networkManager = networkManager; + _imageProcessorFactory = imageProcessorFactory; + _dtoServiceFactory = dtoServiceFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + ConfigurationManager = configurationManager; + _users = Array.Empty(); + + DeletePinFile(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.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 + /// + /// The id. + /// User. + /// + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentException(nameof(id), "Guid can't be empty"); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// + /// Gets the user by identifier. + /// + /// The identifier. + /// User. + public User GetUserById(string id) + { + return GetUserById(new Guid(id)); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public void Initialize() + { + _users = LoadUsers(); + + var users = Users.ToList(); + + // If there are no local users with admin rights, make them all admins + if (!users.Any(i => i.Policy.IsAdministrator)) + { + foreach (var user in users) + { + user.Policy.IsAdministrator = true; + UpdateUserPolicy(user, user.Policy, false); + } + } + } + + public static bool IsValidUsername(string username) + { + // 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 (.) + return Regex.IsMatch(username, @"^[\w\-'._@]*$"); + } + + private static bool IsValidUsernameCharacter(char i) + { + return IsValidUsername(i.ToString()); + } + + public string MakeValidUsername(string username) + { + if (IsValidUsername(username)) + { + return username; + } + + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + var builder = new StringBuilder(); + + foreach (var c in username) + { + if (IsValidUsernameCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + var success = false; + IAuthenticationProvider authenticationProvider = null; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } + + if (user == null) + { + throw new SecurityException("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)); + } + + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + UpdateUser(user); + } + UpdateInvalidLoginAttemptCount(user, 0); + } + else + { + UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + } + + _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); + + return success ? user : null; + } + + private static string GetAuthenticationProviderId(IAuthenticationProvider provider) + { + return provider.GetType().FullName; + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + if (providers.Length == 0) + { + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; + } + + return providers; + } + + private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try + { + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) + { + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + } + else + { + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + + return false; + } + } + + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } + } + } + + if (user != null) + { + 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); + } + } + } + + return new Tuple(authenticationProvider, success); + } + + private void UpdateInvalidLoginAttemptCount(User user, int newValue) + { + 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; + + // -1 can be used to specify no lockout value + if (maxCount != -1 && newValue >= maxCount) + { + _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); + user.Policy.IsDisabled = true; + + fireLockout = true; + } + + UpdateUserPolicy(user, user.Policy, false); + + if (fireLockout) + { + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + } + } + + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : user.EasyPassword; + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + private User[] LoadUsers() + { + var users = UserRepository.RetrieveAllUsers(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyJellyfinUser"; + } + var name = MakeValidUsername(defaultName); + + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + users.Add(user); + + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + UpdateUserPolicy(user, user.Policy, false); + } + + return users.ToArray(); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); + + bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + hasConfiguredEasyPassword : + hasConfiguredPassword; + + UserDto dto = new UserDto + { + Id = user.Id, + Name = user.Name, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword, + HasConfiguredEasyPassword = hasConfiguredEasyPassword, + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration, + ServerId = _appHost.SystemId, + Policy = user.Policy + }; + + if (!hasPassword && Users.Count() == 1) + { + dto.EnableAutoLogin = true; + } + + ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + dto.PrimaryImageTag = GetImageCacheTag(user, image); + + try + { + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); + } + } + + return dto; + } + + public UserDto GetOfflineUserDto(User user) + { + var dto = GetUserDto(user); + + dto.ServerName = _appHost.FriendlyName; + + return dto; + } + + private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + { + try + { + return _imageProcessorFactory().GetImageCacheTag(item, image); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); + return null; + } + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// Task. + public async Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + foreach (var user in Users) + { + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.UpdateUser(user); + + OnUserUpdated(user); + } + + public event EventHandler> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task CreateUser(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + 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 list = Users.ToList(); + list.Add(user); + _users = list.ToArray(); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var allUsers = Users.ToList(); + + if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + { + 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)); + } + + if (allUsers.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)); + } + + if (user.Policy.IsAdministrator && allUsers.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)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + 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); + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Resets the password by clearing it. + /// + /// Task. + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (newPassword != null) + { + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + private static User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = Guid.NewGuid(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + UsesIdForConfigurationPath = true, + //Salt = BCrypt.GenerateSalt() + }; + } + + private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private async Task CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + + var localExpirationTime = expiration.ToLocalTime(); + // Tuesday, 22 August 2006 06:30 AM + text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); + + File.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + DeletePinFile(); + + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = await CreatePasswordResetPin().ConfigureAwait(false); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + foreach (var user in Users) + { + await ResetPassword(user).ConfigureAwait(false); + + if (user.Policy.IsDisabled) + { + user.Policy.IsDisabled = false; + UpdateUserPolicy(user, user.Policy, true); + } + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + _fileSystem.DeleteFile(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + if (!File.Exists(path)) + { + return GetDefaultPolicy(user); + } + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (IOException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return GetDefaultPolicy(user); + } + } + + private static UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableContentDownloading = true, + EnableSyncTranscoding = true + }; + } + + private readonly object _policySyncLock = new object(); + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + UpdateUserPolicy(user, userPolicy, true); + } + + private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) + { + // The xml serializer will output differently if the type is not exact + if (userPolicy.GetType() != typeof(UserPolicy)) + { + var json = _jsonSerializer.SerializeToString(userPolicy); + userPolicy = _jsonSerializer.DeserializeFromString(json); + } + + var path = GetPolicyFilePath(user); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + if (fireEvent) + { + UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + try + { + lock (_policySyncLock) + { + _fileSystem.DeleteFile(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting policy file"); + } + } + + private static string GetPolicyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private static string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + if (!File.Exists(path)) + { + return new UserConfiguration(); + } + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (IOException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + UpdateConfiguration(user, config); + } + + public void UpdateConfiguration(User user, UserConfiguration config) + { + UpdateConfiguration(user, config, true); + } + + private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) + { + var path = GetConfigurationFilePath(user); + + // The xml serializer will output differently if the type is not exact + if (config.GetType() != typeof(UserConfiguration)) + { + var json = _jsonSerializer.SerializeToString(config); + config = _jsonSerializer.DeserializeFromString(json); + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + } + + public class DeviceAccessEntryPoint : IServerEntryPoint + { + private IUserManager _userManager; + private IAuthenticationRepository _authRepo; + private IDeviceManager _deviceManager; + private ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + + return Task.CompletedTask; + } + + private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.Policy.EnableAllDevices) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + + public void Dispose() + { + + } + } +} From 752d65d0204ec474bfce0d2d949bd1bee9e1f89b Mon Sep 17 00:00:00 2001 From: Vasily Date: Tue, 19 Mar 2019 17:13:27 +0300 Subject: [PATCH 0234/6208] Require access type to be included in bug report Inspired by https://github.com/jellyfin/jellyfin/issues/1085#issuecomment-473833591 It seems that the issue with "setup wizard" described there is only prominent when reverse-proxied, not when accessed directly. So this type of information should be gathered in the bug report as well. --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 137a689e8b..0478482685 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,6 +30,7 @@ assignees: '' - OS: [e.g. Docker, Debian, Windows] - Browser: [e.g. Firefox, Chrome, Safari] - Jellyfin Version: [e.g. 10.0.1] + - Jellyfin access: [direct, reverse proxy via nginx, reverse proxy via apache, etc.] **Additional context** From f73d8a44df6302be59db7aaad6117985b3f5683a Mon Sep 17 00:00:00 2001 From: Vasily Date: Tue, 19 Mar 2019 17:37:56 +0300 Subject: [PATCH 0235/6208] Improve the wording per @joshuaboniface suggestion --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0478482685..ca89c1cb91 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ assignees: '' - OS: [e.g. Docker, Debian, Windows] - Browser: [e.g. Firefox, Chrome, Safari] - Jellyfin Version: [e.g. 10.0.1] - - Jellyfin access: [direct, reverse proxy via nginx, reverse proxy via apache, etc.] + - Reverse proxy: [e.g. no, nginx, apache, etc.] **Additional context** From 4cd8903abc1e0c19d717e2930382c37836706e88 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 19 Mar 2019 23:13:02 -0400 Subject: [PATCH 0236/6208] Fix default value for Expires header --- .../HttpServer/HttpResultFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 4632658626..134f3c8418 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires)) { - responseHeaders[HeaderNames.Expires] = "-1"; + responseHeaders[HeaderNames.Expires] = "0"; } AddResponseHeaders(result, responseHeaders); @@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.HttpServer if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders[HeaderNames.Expires] = "-1"; + responseHeaders[HeaderNames.Expires] = "0"; } AddResponseHeaders(result, responseHeaders); @@ -190,7 +190,7 @@ namespace Emby.Server.Implementations.HttpServer if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders[HeaderNames.Expires] = "-1"; + responseHeaders[HeaderNames.Expires] = "0"; } AddResponseHeaders(result, responseHeaders); @@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); } - responseHeaders[HeaderNames.Expires] = "-1"; + responseHeaders[HeaderNames.Expires] = "0"; return ToOptimizedResultInternal(requestContext, result, responseHeaders); } From bd31091648078a9bb295e4be198b853b66b07e1d Mon Sep 17 00:00:00 2001 From: Torsten Date: Wed, 20 Mar 2019 20:00:23 +0100 Subject: [PATCH 0237/6208] Update init scripts for compatibility with Devuan Include start, stop, restart and status option for /etc/init.d/jellyfin Use start-stop-daemon to make the script refer to systemctl mechanism on systems that have systemd installed --- .../debian-package-x64/pkg-src/jellyfin.init | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.init b/deployment/debian-package-x64/pkg-src/jellyfin.init index d103fb0f12..7f5642bac1 100644 --- a/deployment/debian-package-x64/pkg-src/jellyfin.init +++ b/deployment/debian-package-x64/pkg-src/jellyfin.init @@ -8,42 +8,54 @@ # Description: Runs Jellyfin Server ### END INIT INFO +set -e + # Carry out specific functions when asked to by the system -pidfile="/var/run/jellyfin.pid" -pid=`cat $pidfile` +if test -f /etc/default/jellyfin; then + . /etc/default/jellyfin +fi + +. /lib/lsb/init-functions + +PIDFILE="/run/jellyfin.pid" case "$1" in start) - if [ "$pid" == "" ]; then - echo "Starting Jellyfin..." - . /etc/default/jellyfin - nohup su -u $JELLYFIN_USER -c /usr/bin/jellyfin $JELLYFIN_ARGS - echo ?? > $pidfile - else - echo "Jellyfin already running" - fi + log_daemon_msg "Starting Jellyfin Media Server" "jellyfin" || true + + if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then + log_end_msg 0 || true + else + log_end_msg 1 || true + fi ;; + stop) - if [ "$pid" != "" ]; then - echo "Stopping Jellyfin..." - kill $pid - sleep 2 - rm -f $pidfile - else - echo "Jellyfin not running" - fi + log_daemon_msg "Stopping Jellyfin Media Server" "jellyfin" || true + if start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE --remove-pidfile; then + log_end_msg 0 || true + else + log_end_msg 1 || true + fi ;; + + restart) + log_daemon_msg "Restarting Jellyfin Media Server" "jellyfin" || true + start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE --remove-pidfile + if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then + log_end_msg 0 || true + else + log_end_msg 1 || true + fi + ;; + status) - if [ "$pid" != "" ]; then - echo "Jellyfin running as $pid" - ps -f $pid - else - echo "Jellyfin is not running" - fi + status_of_proc -p $PIDFILE /usr/bin/jellyfin jellyfin && exit 0 || exit $? ;; + *) - echo "Usage: $0 {start|stop}" + echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac From 949a1cce214bf24c534f9821e26cafa707600012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Wed, 20 Mar 2019 21:20:16 +0100 Subject: [PATCH 0238/6208] Generate doxygen template config `doxygen -g` --- .gitignore | 3 + Doxyfile | 2565 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2568 insertions(+) create mode 100644 Doxyfile diff --git a/.gitignore b/.gitignore index 65e47747ee..a1b7ac59c8 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,6 @@ deployment/collect-dist/ jellyfin_version.ini ci/ + +# Doxygen +doc/ \ No newline at end of file diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000000..20231496c8 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,2565 @@ +# Doxyfile 1.8.15 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "My Project" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat +# .inc files as Fortran files (default is PHP), and .f files as C (default is +# Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 0. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the +# cost of reduced performance. This can be particularly helpful with template +# rich C++ code for which doxygen's built-in parser lacks the necessary type +# information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files +# were built. This is equivalent to specifying the "-p" option to a clang tool, +# such as clang-check. These options will then be passed to the parser. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via Javascript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have Javascript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/ + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , / /// The message. /// Task. - public Task ProcessMessage(WebSocketMessageInfo message) - { - return Task.CompletedTask; - } + public Task ProcessMessageAsync(WebSocketMessageInfo message) + => Task.CompletedTask; private void EnsureController(SessionInfo session, IWebSocketConnection connection) { diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index 373f6d7580..ec637186f0 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -86,8 +86,7 @@ namespace Emby.Server.Implementations.SocketSharp else { // We use a substream, as in 2.x we will support large uploads streamed to disk, - var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files[e.Name] = sub; + files[e.Name] = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); } } } @@ -374,7 +373,7 @@ namespace Emby.Server.Implementations.SocketSharp var elem = new Element(); ReadOnlySpan header; - while ((header = ReadHeaders().AsSpan()) != null) + while ((header = ReadLine().AsSpan()).Length != 0) { if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { @@ -513,17 +512,6 @@ namespace Emby.Server.Implementations.SocketSharp return false; } - private string ReadHeaders() - { - string s = ReadLine(); - if (s.Length == 0) - { - return null; - } - - return s; - } - private static bool CompareBytes(byte[] orig, byte[] other) { for (int i = orig.Length - 1; i >= 0; i--) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index e0a0ee2861..6fdc6a3c8d 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Linq; using System.Text; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; @@ -474,27 +474,28 @@ namespace Emby.Server.Implementations.SocketSharp { get { - if (httpFiles == null) + if (httpFiles != null) { - if (files == null) - { - return httpFiles = Array.Empty(); - } + return httpFiles; + } - httpFiles = new IHttpFile[files.Count]; - var i = 0; - foreach (var pair in files) + if (files == null) + { + return httpFiles = Array.Empty(); + } + + var values = files.Values; + httpFiles = new IHttpFile[values.Count]; + for (int i = 0; i < values.Count; i++) + { + var reqFile = values.ElementAt(i); + httpFiles[i] = new HttpFile { - var reqFile = pair.Value; - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - i++; - } + ContentType = reqFile.ContentType, + ContentLength = reqFile.ContentLength, + FileName = reqFile.FileName, + InputStream = reqFile.InputStream, + }; } return httpFiles; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 82a76c6378..fbeb7a2e82 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -37,7 +37,7 @@ namespace Jellyfin.Server private static bool _restartOnShutdown; private static IConfiguration appConfig; - public static async Task Main(string[] args) + public static Task Main(string[] args) { // For backwards compatibility. // Modify any input arguments now which start with single-hyphen to POSIX standard @@ -51,8 +51,8 @@ namespace Jellyfin.Server } // Parse the command line arguments and either start the app or exit indicating error - await Parser.Default.ParseArguments(args) - .MapResult(StartApp, _ => Task.CompletedTask).ConfigureAwait(false); + return Parser.Default.ParseArguments(args) + .MapResult(StartApp, _ => Task.CompletedTask); } public static void Shutdown() diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 8eefbdf2ca..8a5a793dfd 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -988,19 +988,16 @@ namespace MediaBrowser.Api.Library /// Posts the specified request. /// /// The request. - public void Post(RefreshLibrary request) + public async Task Post(RefreshLibrary request) { - Task.Run(() => + try { - try - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error refreshing library"); - } - }); + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error refreshing library"); + } } /// diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 8444125462..ee5c1a165a 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Controller.Net /// /// The message. /// Task. - public Task ProcessMessage(WebSocketMessageInfo message) + public Task ProcessMessageAsync(WebSocketMessageInfo message) { if (message == null) { @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.Net Stop(message); } - return Task.FromResult(true); + return Task.CompletedTask; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs index e38f0e259f..0f472a2bc7 100644 --- a/MediaBrowser.Controller/Net/IWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -12,6 +12,6 @@ namespace MediaBrowser.Controller.Net /// /// The message. /// Task. - Task ProcessMessage(WebSocketMessageInfo message); + Task ProcessMessageAsync(WebSocketMessageInfo message); } } diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index 18712470da..76d816e7bd 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -23,8 +23,6 @@ namespace Rssdp.Infrastructure #region Public Methods - private static byte[] EmptyByteArray = new byte[]{}; - /// /// Parses the provided into either a or object. /// @@ -46,7 +44,7 @@ namespace Rssdp.Infrastructure if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", nameof(data)); if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data)); - using (var retVal = new ByteArrayContent(EmptyByteArray)) + using (var retVal = new ByteArrayContent(Array.Empty())) { var lines = data.Split(LineTerminators, StringSplitOptions.None); @@ -209,4 +207,4 @@ namespace Rssdp.Infrastructure #endregion } -} \ No newline at end of file +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index d9a4b6ac01..5d2afc37a0 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -355,7 +355,7 @@ namespace Rssdp.Infrastructure { var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort); - ListenToSocket(socket); + _ = ListenToSocketInternal(socket); return socket; } @@ -389,19 +389,12 @@ namespace Rssdp.Infrastructure foreach (var socket in sockets) { - ListenToSocket(socket); + _ = ListenToSocketInternal(socket); } return sockets; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capturing task to local variable removes compiler warning, task is not otherwise required.")] - private void ListenToSocket(ISocket socket) - { - // Tasks are captured to local variables even if we don't use them just to avoid compiler warnings. - var t = Task.Run(() => ListenToSocketInternal(socket)); - } - private async Task ListenToSocketInternal(ISocket socket) { var cancelled = false; @@ -448,10 +441,10 @@ namespace Rssdp.Infrastructure private void ProcessMessage(string data, IpEndPointInfo endPoint, IpAddressInfo receivedOnLocalIpAddress) { - //Responses start with the HTTP version, prefixed with HTTP/ while - //requests start with a method which can vary and might be one we haven't - //seen/don't know. We'll check if this message is a request or a response - //by checking for the HTTP/ prefix on the start of the message. + // Responses start with the HTTP version, prefixed with HTTP/ while + // requests start with a method which can vary and might be one we haven't + // seen/don't know. We'll check if this message is a request or a response + // by checking for the HTTP/ prefix on the start of the message. if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) { HttpResponseMessage responseMessage = null; @@ -465,7 +458,9 @@ namespace Rssdp.Infrastructure } if (responseMessage != null) + { OnResponseReceived(responseMessage, endPoint, receivedOnLocalIpAddress); + } } else { From 4c8f8cf64cc81fb5f1bb32aeb8349ce38badf457 Mon Sep 17 00:00:00 2001 From: Phlogi Date: Mon, 25 Mar 2019 21:34:55 +0100 Subject: [PATCH 0267/6208] Removed trailing spaces, renamed get wan IP function. --- Emby.Server.Implementations/ApplicationHost.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7c8fb7f62b..b5582ae4fd 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1376,7 +1376,7 @@ namespace Emby.Server.Implementations if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false); + wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { @@ -1435,7 +1435,7 @@ namespace Emby.Server.Implementations if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false); + wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { @@ -1478,7 +1478,7 @@ namespace Emby.Server.Implementations return null; } - public async Task GetWanApiUrl(CancellationToken cancellationToken) + public async Task GetWanApiUrlFromExternal(CancellationToken cancellationToken) { const string Url = "http://ipv4.icanhazip.com"; try @@ -1524,7 +1524,7 @@ namespace Emby.Server.Implementations } return string.Format("http://{0}:{1}", host, - HttpPort.ToString(CultureInfo.InvariantCulture)); + HttpPort.ToString(CultureInfo.InvariantCulture)); } public string GetWanApiUrl(IpAddressInfo ipAddress) From 1b03f078b92021b8f3c2e7c148f1e67debd27fac Mon Sep 17 00:00:00 2001 From: Phlogi Date: Mon, 25 Mar 2019 21:43:50 +0100 Subject: [PATCH 0268/6208] No need to assign empty string. --- Emby.Server.Implementations/ApplicationHost.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b5582ae4fd..49da672923 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1372,15 +1372,14 @@ namespace Emby.Server.Implementations public async Task GetSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var wanAddress = string.Empty; if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); + var wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { - wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); + var wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); } return new SystemInfo @@ -1431,15 +1430,14 @@ namespace Emby.Server.Implementations public async Task GetPublicSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var wanAddress = string.Empty; if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); + var wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { - wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); + var wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); } return new PublicSystemInfo { From b44a70ff368f228fc27a184e2139d288bada85cc Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 25 Mar 2019 22:25:32 +0100 Subject: [PATCH 0269/6208] Simplify/remove/clean code * Remove useless runtime check (we only support one) * Remove unused args * Remove a global constant And ofc fix some warnings ;) --- .../ApplicationHost.cs | 18 ++++------- .../Cryptography/CryptographyProvider.cs | 3 +- .../SqliteDisplayPreferencesRepository.cs | 3 +- .../EntryPoints/LibraryChangedNotifier.cs | 4 +-- .../HttpServer/Security/AuthService.cs | 4 +-- .../HttpServer/StreamWriter.cs | 3 -- .../LiveTv/EmbyTV/EmbyTV.cs | 4 +-- .../LiveTv/LiveTvManager.cs | 4 +-- .../LiveTv/TunerHosts/M3UTunerHost.cs | 14 +++----- .../LiveTv/TunerHosts/M3uParser.cs | 20 +++++++----- .../Updates/InstallationManager.cs | 32 ++++--------------- Jellyfin.Server/Program.cs | 1 - .../Extensions/BaseExtensions.cs | 10 +++--- jellyfin.ruleset | 3 ++ 14 files changed, 50 insertions(+), 73 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 484942946c..e15cb68e9e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -200,7 +200,7 @@ namespace Emby.Server.Implementations /// /// The disposable parts /// - protected readonly List _disposableParts = new List(); + private readonly List _disposableParts = new List(); /// /// Gets the configuration manager. @@ -216,8 +216,9 @@ namespace Emby.Server.Implementations { #if BETA return PackageVersionClass.Beta; -#endif +#else return PackageVersionClass.Release; +#endif } } @@ -340,7 +341,6 @@ namespace Emby.Server.Implementations protected IProcessFactory ProcessFactory { get; private set; } - protected ICryptoProvider CryptographyProvider = new CryptographyProvider(); protected readonly IXmlSerializer XmlSerializer; protected ISocketFactory SocketFactory { get; private set; } @@ -369,9 +369,6 @@ namespace Emby.Server.Implementations { _configuration = configuration; - // hack alert, until common can target .net core - BaseExtensions.CryptographyProvider = CryptographyProvider; - XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory); NetworkManager = networkManager; @@ -735,13 +732,12 @@ namespace Emby.Server.Implementations ApplicationHost.StreamHelper = new StreamHelper(); serviceCollection.AddSingleton(StreamHelper); - serviceCollection.AddSingleton(CryptographyProvider); + serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider)); SocketFactory = new SocketFactory(); serviceCollection.AddSingleton(SocketFactory); - InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime); - serviceCollection.AddSingleton(InstallationManager); + serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager)); ZipClient = new ZipClient(); serviceCollection.AddSingleton(ZipClient); @@ -908,8 +904,6 @@ namespace Emby.Server.Implementations _serviceProvider = serviceCollection.BuildServiceProvider(); } - public virtual string PackageRuntime => "netcore"; - public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) { // Distinct these to prevent users from reporting problems that aren't actually problems @@ -1049,6 +1043,8 @@ namespace Emby.Server.Implementations /// protected void FindParts() { + InstallationManager = _serviceProvider.GetService(); + if (!ServerConfigurationManager.Configuration.IsPortAuthorized) { ServerConfigurationManager.Configuration.IsPortAuthorized = true; diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 982bba625d..6d7193ce20 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; -using System.Linq; using MediaBrowser.Model.Cryptography; namespace Emby.Server.Implementations.Cryptography @@ -136,7 +135,7 @@ namespace Emby.Server.Implementations.Cryptography { return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); } - + public byte[] ComputeHash(PasswordHash hash) { int iterations = _defaultIterations; diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 3d60925da3..47552806d4 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -90,9 +90,10 @@ namespace Emby.Server.Implementations.Data { throw new ArgumentNullException(nameof(displayPreferences)); } + if (string.IsNullOrEmpty(displayPreferences.Id)) { - throw new ArgumentNullException(nameof(displayPreferences.Id)); + throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); } cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 0389656477..8369f4f593 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), - CollectionFolders = GetTopParentIds(newAndRemoved, user, allUserRootChildren).ToArray() + CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() }; } @@ -407,7 +407,7 @@ namespace Emby.Server.Implementations.EntryPoints return item.SourceType == SourceType.Library; } - private IEnumerable GetTopParentIds(List items, User user, List allUserRootChildren) + private IEnumerable GetTopParentIds(List items, List allUserRootChildren) { var list = new List(); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 499a334fc9..1027883ed9 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security // This code is executed before the service var auth = AuthorizationContext.GetAuthorizationInfo(request); - if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request)) + if (!IsExemptFromAuthenticationToken(authAttribtues, request)) { ValidateSecurityToken(request, auth.Token); } @@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security } } - private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request) + private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request) { if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) { diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index cf30bbc326..324f9085e5 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -14,8 +13,6 @@ namespace Emby.Server.Implementations.HttpServer /// public class StreamWriter : IAsyncStreamWriter, IHasHeaders { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// /// Gets or sets the source stream. /// diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 58b3b6a69f..7b210d2313 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; - public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress progress) + public async Task RefreshSeriesTimers(CancellationToken cancellationToken) { var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); @@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public async Task RefreshTimers(CancellationToken cancellationToken, IProgress progress) + public async Task RefreshTimers(CancellationToken cancellationToken) { var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index f7ef16fb09..9093d9740f 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1087,8 +1087,8 @@ namespace Emby.Server.Implementations.LiveTv if (coreService != null) { - await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress()).ConfigureAwait(false); - await coreService.RefreshTimers(cancellationToken, new SimpleProgress()).ConfigureAwait(false); + await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false); + await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false); } // Load these now which will prefetch metadata diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 588dcb843b..2d9bec53f0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -10,14 +10,12 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -52,9 +50,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); - - return result.Cast().ToList(); + return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); } public Task> GetTunerInfos(CancellationToken cancellationToken) @@ -73,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private string[] _disallowedSharedStreamExtensions = new string[] + private static readonly string[] _disallowedSharedStreamExtensions = new string[] { ".mkv", ".mp4", @@ -88,9 +84,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (tunerCount > 0) { var tunerHostId = info.Id; - var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)).ToList(); + var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); - if (liveStreams.Count >= tunerCount) + if (liveStreams.Count() >= tunerCount) { throw new LiveTvConflictException("M3U simultaneous stream limit has been reached."); } @@ -98,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false); - var mediaSource = sources.First(); + var mediaSource = sources[0]; if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index ad124bb0ff..814031b126 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -11,7 +11,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts @@ -62,12 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult((Stream)File.OpenRead(url)); } - const string ExtInfPrefix = "#EXTINF:"; + private const string ExtInfPrefix = "#EXTINF:"; + private List GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId) { var channels = new List(); string line; - string extInf = ""; + string extInf = string.Empty; while ((line = reader.ReadLine()) != null) { @@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts channel.Path = line; channels.Add(channel); - extInf = ""; + extInf = string.Empty; } } @@ -110,8 +110,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl) { - var channel = new ChannelInfo(); - channel.TunerHostId = tunerHostId; + var channel = new ChannelInfo() + { + TunerHostId = tunerHostId + }; extInf = extInf.Trim(); @@ -137,13 +139,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { channelIdValues.Add(channelId); } + if (!string.IsNullOrWhiteSpace(tvgId)) { channelIdValues.Add(tvgId); } + if (channelIdValues.Count > 0) { - channel.Id = string.Join("_", channelIdValues.ToArray()); + channel.Id = string.Join("_", channelIdValues); } return channel; @@ -152,7 +156,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetChannelNumber(string extInf, Dictionary attributes, string mediaUrl) { var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null; + var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null; string numberString = null; string attributeValue; diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 301802b8a6..7310de55d4 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -12,7 +12,6 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -39,11 +38,10 @@ namespace Emby.Server.Implementations.Updates /// /// The completed installations /// - private ConcurrentBag CompletedInstallationsInternal { get; set; } + private ConcurrentBag _completedInstallationsInternal; - public IEnumerable CompletedInstallations => CompletedInstallationsInternal; + public IEnumerable CompletedInstallations => _completedInstallationsInternal; - #region PluginUninstalled Event /// /// Occurs when [plugin uninstalled]. /// @@ -57,9 +55,7 @@ namespace Emby.Server.Implementations.Updates { PluginUninstalled?.Invoke(this, new GenericEventArgs { Argument = plugin }); } - #endregion - #region PluginUpdated Event /// /// Occurs when [plugin updated]. /// @@ -77,9 +73,7 @@ namespace Emby.Server.Implementations.Updates _applicationHost.NotifyPendingRestart(); } - #endregion - #region PluginInstalled Event /// /// Occurs when [plugin updated]. /// @@ -96,7 +90,6 @@ namespace Emby.Server.Implementations.Updates _applicationHost.NotifyPendingRestart(); } - #endregion /// /// The _logger @@ -115,12 +108,8 @@ namespace Emby.Server.Implementations.Updates /// The application host. private readonly IApplicationHost _applicationHost; - private readonly ICryptoProvider _cryptographyProvider; private readonly IZipClient _zipClient; - // netframework or netcore - private readonly string _packageRuntime; - public InstallationManager( ILoggerFactory loggerFactory, IApplicationHost appHost, @@ -129,9 +118,7 @@ namespace Emby.Server.Implementations.Updates IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, - ICryptoProvider cryptographyProvider, - IZipClient zipClient, - string packageRuntime) + IZipClient zipClient) { if (loggerFactory == null) { @@ -139,18 +126,16 @@ namespace Emby.Server.Implementations.Updates } CurrentInstallations = new List>(); - CompletedInstallationsInternal = new ConcurrentBag(); + _completedInstallationsInternal = new ConcurrentBag(); + _logger = loggerFactory.CreateLogger(nameof(InstallationManager)); _applicationHost = appHost; _appPaths = appPaths; _httpClient = httpClient; _jsonSerializer = jsonSerializer; _config = config; _fileSystem = fileSystem; - _cryptographyProvider = cryptographyProvider; _zipClient = zipClient; - _packageRuntime = packageRuntime; - _logger = loggerFactory.CreateLogger(nameof(InstallationManager)); } private static Version GetPackageVersion(PackageVersionInfo version) @@ -222,11 +207,6 @@ namespace Emby.Server.Implementations.Updates continue; } - if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1) - { - continue; - } - versions.Add(version); } @@ -448,7 +428,7 @@ namespace Emby.Server.Implementations.Updates CurrentInstallations.Remove(tuple); } - CompletedInstallationsInternal.Add(installationInfo); + _completedInstallationsInternal.Add(installationInfo); PackageInstallationCompleted?.Invoke(this, installationEventArgs); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 82a76c6378..d4b10c8c84 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index db0514bb18..40c16b9573 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -1,6 +1,7 @@ using System; +using System.Text; using System.Text.RegularExpressions; -using MediaBrowser.Model.Cryptography; +using System.Security.Cryptography; namespace MediaBrowser.Common.Extensions { @@ -9,8 +10,6 @@ namespace MediaBrowser.Common.Extensions /// public static class BaseExtensions { - public static ICryptoProvider CryptographyProvider { get; set; } - /// /// Strips the HTML. /// @@ -31,7 +30,10 @@ namespace MediaBrowser.Common.Extensions /// Guid. public static Guid GetMD5(this string str) { - return CryptographyProvider.GetMD5(str); + using (var provider = MD5.Create()) + { + return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str))); + } } } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 0a04b4c557..262121a325 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -20,6 +20,9 @@ + + + From 122cba2aa755df7d7c3e63aaa441f639b126b55c Mon Sep 17 00:00:00 2001 From: Phlogi Date: Mon, 25 Mar 2019 22:26:05 +0100 Subject: [PATCH 0270/6208] Correct use of local variable wanAddress. --- Emby.Server.Implementations/ApplicationHost.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 49da672923..fd14cb89d7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1373,13 +1373,15 @@ namespace Emby.Server.Implementations { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + string wanAddress; + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - var wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); + wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { - var wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); + wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); } return new SystemInfo @@ -1431,13 +1433,15 @@ namespace Emby.Server.Implementations { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + string wanAddress; + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { - var wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); + wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); } else { - var wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); + wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); } return new PublicSystemInfo { From 740c95d557515cedd3912983f7aec50bdfefb0d4 Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Mon, 25 Mar 2019 21:40:10 -0700 Subject: [PATCH 0271/6208] Apply minor suggestions from code review Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultPasswordResetProvider.cs | 223 +++++++++--------- .../Library/UserManager.cs | 2 +- 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 1ae8960eea..46f3732d68 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,113 +1,114 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - public string Name => "Default Password Reset Provider"; - - public bool IsEnabled => true; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - - private IJsonSerializer _jsonSerializer; - private IUserManager _userManager; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - public async Task RedeemPasswordResetPin(string pin) - { - HashSet usersreset = new HashSet(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) - { - var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetfile); - } - else - { - if (spr.Pin == pin) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (!string.IsNullOrEmpty(resetUser.Password)) - { - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - } - } - } - } - - if (usersreset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - else - { - return new PinRedeemResult - { - Success = true, - UsersReset = usersreset.ToArray() - }; - } - throw new System.NotImplementedException(); - } - - public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) - { - string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); - DateTime expireTime = DateTime.Now.AddMinutes(30); - string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Name - }; - - try - { - await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); - } - catch (Exception e) - { - throw new Exception($"Error serializing or writing password reset for {user.Name} to location:{filePath}", e); - } - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - PinFile = filePath - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; - public string UserName { get; set; } - } - } +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + public string Name => "Default Password Reset Provider"; + + public bool IsEnabled => true; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + private readonly string _passwordResetFileBaseName = "passwordreset"; + + private IJsonSerializer _jsonSerializer; + private IUserManager _userManager; + + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + public async Task RedeemPasswordResetPin(string pin) + { + HashSet usersreset = new HashSet(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + { + var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetfile); + } + else + { + if (spr.Pin == pin) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (!string.IsNullOrEmpty(resetUser.Password)) + { + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + } + } + } + } + + if (usersreset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + else + { + return new PinRedeemResult + { + Success = true, + UsersReset = usersreset.ToArray() + }; + } + + throw new System.NotImplementedException(); + } + + public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) + { + string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); + DateTime expireTime = DateTime.Now.AddMinutes(30); + string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = filePath, + UserName = user.Name + }; + + try + { + await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); + } + catch (Exception e) + { + throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e); + } + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + PinFile = filePath + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 05ec750ba9..75c82ca715 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -373,7 +373,7 @@ namespace Emby.Server.Implementations.Library private IPasswordResetProvider GetPasswordResetProvider(User user) { - return GetPasswordResetProviders(user).First(); + return GetPasswordResetProviders(user)[0]; } private IAuthenticationProvider[] GetAuthenticationProviders(User user) From 6be8624373bba6cf25a659390874613a4ea6ba79 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 25 Mar 2019 22:17:23 -0700 Subject: [PATCH 0272/6208] async improvements and post reset cleanups --- .../Library/DefaultPasswordResetProvider.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 46f3732d68..a589d61689 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; +using Microsoft.Win32.SafeHandles; namespace Emby.Server.Implementations.Library { @@ -39,21 +40,19 @@ namespace Emby.Server.Implementations.Library HashSet usersreset = new HashSet(); foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) { - var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); + var spr = await _jsonSerializer.DeserializeFromStreamAsync(File.OpenRead(resetfile)).ConfigureAwait(false); if (spr.ExpirationDate < DateTime.Now) { File.Delete(resetfile); } - else + else if (spr.Pin == pin) { - if (spr.Pin == pin) + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser != null) { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (!string.IsNullOrEmpty(resetUser.Password)) - { - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - } + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + File.Delete(resetfile); } } } @@ -70,15 +69,13 @@ namespace Emby.Server.Implementations.Library UsersReset = usersreset.ToArray() }; } - - throw new System.NotImplementedException(); } public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); DateTime expireTime = DateTime.Now.AddMinutes(30); - string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; + string filePath = _passwordResetFileBase + user.InternalId + ".json"; SerializablePasswordReset spr = new SerializablePasswordReset { ExpirationDate = expireTime, @@ -88,8 +85,10 @@ namespace Emby.Server.Implementations.Library }; try - { - await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); + { + FileStream fileStream = File.OpenWrite(filePath); + _jsonSerializer.SerializeToStream(spr,fileStream); + await fileStream.FlushAsync().ConfigureAwait(false); } catch (Exception e) { From a332092769cec5a3b17b7fb49b2d7c66bfe289bd Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Mar 2019 19:20:40 +0100 Subject: [PATCH 0273/6208] Reduce complexity http routes --- .../ApplicationHost.cs | 37 +----- .../HttpServer/HttpListenerHost.cs | 50 +++----- .../Services/ServiceController.cs | 62 ++++------ .../Services/ServiceHandler.cs | 107 +++++++++--------- .../Services/SwaggerService.cs | 20 ++-- Jellyfin.Server/Program.cs | 5 +- 6 files changed, 111 insertions(+), 170 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 484942946c..ce08f2a284 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -617,8 +617,6 @@ namespace Emby.Server.Implementations DiscoverTypes(); - SetHttpLimit(); - await RegisterResources(serviceCollection).ConfigureAwait(false); FindParts(); @@ -918,8 +916,7 @@ namespace Emby.Server.Implementations .Distinct(); logger.LogInformation("Arguments: {Args}", commandLineArgs); - // FIXME: @bond this logs the kernel version, not the OS version - logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version); + logger.LogInformation("Operating system: {OS}", OperatingSystem.Name); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); @@ -929,19 +926,6 @@ namespace Emby.Server.Implementations logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); } - private void SetHttpLimit() - { - try - { - // Increase the max http request limit - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error setting http limit"); - } - } - private X509Certificate2 GetCertificate(CertificateInfo info) { var certificateLocation = info?.Path; @@ -1483,6 +1467,7 @@ namespace Emby.Server.Implementations { Logger.LogError(ex, "Error getting WAN Ip address information"); } + return null; } @@ -1756,24 +1741,6 @@ namespace Emby.Server.Implementations { } - /// - /// Called when [application updated]. - /// - /// The package. - protected void OnApplicationUpdated(PackageVersionInfo package) - { - Logger.LogInformation("Application has been updated to version {0}", package.versionStr); - - ApplicationUpdated?.Invoke( - this, - new GenericEventArgs() - { - Argument = package - }); - - NotifyPendingRestart(); - } - private bool _disposed = false; /// diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e8d47cad52..79b8f52d78 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net.Sockets; @@ -11,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -127,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer private List GetRequestFilterAttributes(Type requestDtoType) { - var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType().ToList(); + var attributes = requestDtoType.GetCustomAttributes(true).OfType().ToList(); var serviceType = GetServiceTypeByRequest(requestDtoType); if (serviceType != null) { - attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType()); + attributes.AddRange(serviceType.GetCustomAttributes(true).OfType()); } attributes.Sort((x, y) => x.Priority - y.Priority); @@ -154,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer QueryString = e.QueryString ?? new QueryCollection() }; - connection.Closed += Connection_Closed; + connection.Closed += OnConnectionClosed; lock (_webSocketConnections) { @@ -164,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); } - private void Connection_Closed(object sender, EventArgs e) + private void OnConnectionClosed(object sender, EventArgs e) { lock (_webSocketConnections) { @@ -322,14 +320,14 @@ namespace Emby.Server.Implementations.HttpServer private static string NormalizeConfiguredLocalAddress(string address) { - var index = address.Trim('/').IndexOf('/'); - + var add = address.AsSpan().Trim('/'); + int index = add.IndexOf('/'); if (index != -1) { - address = address.Substring(index + 1); + add = add.Slice(index + 1); } - return address.Trim('/'); + return add.TrimStart('/').ToString(); } private bool ValidateHost(string host) @@ -399,8 +397,8 @@ namespace Emby.Server.Implementations.HttpServer if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) { // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 || - urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) + if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 + || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) { return true; } @@ -572,7 +570,7 @@ namespace Emby.Server.Implementations.HttpServer if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false); } else { @@ -613,21 +611,11 @@ namespace Emby.Server.Implementations.HttpServer { var pathInfo = httpReq.PathInfo; - var pathParts = pathInfo.TrimStart('/').Split('/'); - if (pathParts.Length == 0) - { - Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); - return null; - } - - var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType); + pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); + var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); if (restPath != null) { - return new ServiceHandler - { - RestPath = restPath, - ResponseContentType = contentType - }; + return new ServiceHandler(restPath, contentType); } Logger.LogError("Could not find handler for {PathInfo}", pathInfo); @@ -655,11 +643,6 @@ namespace Emby.Server.Implementations.HttpServer } else { - // TODO what is this? - var httpsUrl = url - .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) - .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); - RedirectToUrl(httpRes, url); } } @@ -684,10 +667,7 @@ namespace Emby.Server.Implementations.HttpServer UrlPrefixes = urlPrefixes.ToArray(); ServiceController = new ServiceController(); - Logger.LogInformation("Calling ServiceStack AppHost.Init"); - - var types = services.Select(r => r.GetType()).ToArray(); - + var types = services.Select(r => r.GetType()); ServiceController.Init(this, types); ResponseFilters = new Action[] diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 5796956d87..5e3d529c68 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -1,26 +1,17 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.Services { - public delegate Task InstanceExecFn(IRequest requestContext, object intance, object request); public delegate object ActionInvokerFn(object intance, object request); public delegate void VoidActionInvokerFn(object intance, object request); public class ServiceController { - public static ServiceController Instance; - - public ServiceController() - { - Instance = this; - } - - public void Init(HttpListenerHost appHost, Type[] serviceTypes) + public void Init(HttpListenerHost appHost, IEnumerable serviceTypes) { foreach (var serviceType in serviceTypes) { @@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services foreach (var mi in serviceType.GetActions()) { var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) continue; + if (processedReqs.Contains(requestType)) + { + continue; + } + processedReqs.Add(requestType); ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); @@ -55,18 +50,6 @@ namespace Emby.Server.Implementations.Services } } - public static Type FirstGenericType(Type type) - { - while (type != null) - { - if (type.GetTypeInfo().IsGenericType) - return type; - - type = type.GetTypeInfo().BaseType; - } - return null; - } - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) @@ -84,17 +67,24 @@ namespace Emby.Server.Implementations.Services public void RegisterRestPath(RestPath restPath) { - if (!restPath.Path.StartsWith("/")) - throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName())); - if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) - throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName())); - - if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) + if (restPath.Path[0] != '/') { - pathsAtFirstMatch = new List(); - RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch; + throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName())); + } + + if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) + { + throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName())); + } + + if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) + { + pathsAtFirstMatch.Add(restPath); + } + else + { + RestPathMap[restPath.FirstMatchHashKey] = new List() { restPath }; } - pathsAtFirstMatch.Add(restPath); } public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) @@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services return null; } - public Task Execute(HttpListenerHost appHost, object requestDto, IRequest req) + public Task Execute(HttpListenerHost httpHost, object requestDto, IRequest req) { req.Dto = requestDto; var requestType = requestDto.GetType(); req.OperationName = requestType.Name; - var serviceType = appHost.GetServiceTypeByRequest(requestType); + var serviceType = httpHost.GetServiceTypeByRequest(requestType); - var service = appHost.CreateInstance(serviceType); - - //var service = typeFactory.CreateInstance(serviceType); + var service = httpHost.CreateInstance(serviceType); var serviceRequiresContext = service as IRequiresRequest; if (serviceRequiresContext != null) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 3c8adfc983..243d2cca27 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -11,6 +11,18 @@ namespace Emby.Server.Implementations.Services { public class ServiceHandler { + private readonly ServiceController _serviceController; + + public RestPath RestPath { get; } + + public string ResponseContentType { get; } + + internal ServiceHandler(RestPath restPath, string responseContentType) + { + RestPath = restPath; + ResponseContentType = responseContentType; + } + protected static Task CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) { if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) @@ -21,21 +33,22 @@ namespace Emby.Server.Implementations.Services return deserializer(requestType, httpReq.InputStream); } } + return Task.FromResult(host.CreateInstance(requestType)); } - public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) + public RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) { pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); - return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo); + return _serviceController.GetRestPathForRequest(httpMethod, pathInfo); } public static string GetSanitizedPathInfo(string pathInfo, out string contentType) { contentType = null; var pos = pathInfo.LastIndexOf('.'); - if (pos >= 0) + if (pos != -1) { var format = pathInfo.Substring(pos + 1); contentType = GetFormatContentType(format); @@ -44,58 +57,38 @@ namespace Emby.Server.Implementations.Services pathInfo = pathInfo.Substring(0, pos); } } + return pathInfo; } private static string GetFormatContentType(string format) { //built-in formats - if (format == "json") - return "application/json"; - if (format == "xml") - return "application/xml"; - - return null; + switch (format) + { + case "json": return "application/json"; + case "xml": return "application/xml"; + default: return null; + } } - public RestPath GetRestPath(string httpMethod, string pathInfo) + public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken) { - if (this.RestPath == null) - { - this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out string contentType); - - if (contentType != null) - ResponseContentType = contentType; - } - return this.RestPath; - } - - public RestPath RestPath { get; set; } - - // Set from SSHHF.GetHandlerForPathInfo() - public string ResponseContentType { get; set; } - - public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName, CancellationToken cancellationToken) - { - var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo); - if (restPath == null) - { - throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo); - } - - SetRoute(httpReq, restPath); + httpReq.Items["__route"] = RestPath; if (ResponseContentType != null) + { httpReq.ResponseContentType = ResponseContentType; + } - var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false); + var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false); - appHost.ApplyRequestFilters(httpReq, httpRes, request); + httpHost.ApplyRequestFilters(httpReq, httpRes, request); - var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false); + var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); // Apply response filters - foreach (var responseFilter in appHost.ResponseFilters) + foreach (var responseFilter in httpHost.ResponseFilters) { responseFilter(httpReq, httpRes, response); } @@ -152,7 +145,11 @@ namespace Emby.Server.Implementations.Services foreach (var name in request.QueryString.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } var values = request.QueryString[name]; if (values.Count == 1) @@ -175,7 +172,11 @@ namespace Emby.Server.Implementations.Services { foreach (var name in formData.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } var values = formData.GetValues(name); if (values.Count == 1) @@ -210,7 +211,12 @@ namespace Emby.Server.Implementations.Services foreach (var name in request.QueryString.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } + map[name] = request.QueryString[name]; } @@ -221,7 +227,12 @@ namespace Emby.Server.Implementations.Services { foreach (var name in formData.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } + map[name] = formData[name]; } } @@ -229,17 +240,5 @@ namespace Emby.Server.Implementations.Services return map; } - - private static void SetRoute(IRequest req, RestPath route) - { - req.Items["__route"] = route; - } - - private static RestPath GetRoute(IRequest req) - { - req.Items.TryGetValue("__route", out var route); - return route as RestPath; - } } - } diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs index 3e6970eefd..d223864364 100644 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ b/Emby.Server.Implementations/Services/SwaggerService.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; +using Emby.Server.Implementations.HttpServer; namespace Emby.Server.Implementations.Services { @@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services public class SwaggerService : IService, IRequiresRequest { + private readonly IHttpServer _httpServer; private SwaggerSpec _spec; public IRequest Request { get; set; } + public SwaggerService(IHttpServer httpServer) + { + _httpServer = httpServer; + } + public object Get(GetSwaggerSpec request) { return _spec ?? (_spec = GetSpec()); @@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services { var paths = new SortedDictionary>(); - var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); + // REVIEW: this can be done better + var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); foreach (var current in all) { @@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services continue; } - if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase)) + if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase) + || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 82a76c6378..fab584befc 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -119,6 +118,10 @@ namespace Jellyfin.Server SQLitePCL.Batteries_V2.Init(); + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); From 7343e07fe587b4817f5e80b68359d3193674e6ab Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Mar 2019 19:31:06 +0100 Subject: [PATCH 0274/6208] Fix build error --- MediaBrowser.Controller/IServerApplicationHost.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 22797aa0d9..81b9ff0a57 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -83,8 +83,6 @@ namespace MediaBrowser.Controller void EnableLoopback(string appName); - string PackageRuntime { get; } - WakeOnLanInfo[] GetWakeOnLanInfo(); string ExpandVirtualPath(string path); From 93e535d3a143d092effb37e529e5682c3d11802a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 26 Mar 2019 22:56:05 +0100 Subject: [PATCH 0275/6208] Trying to make sense of the streaming code Mostly small changes as I was looking through the code. * async void -> async Task * Properly implemented dispose methods * Pass the logstream directly to the JobLogger * Style fixes --- MediaBrowser.Api/ApiEntryPoint.cs | 15 +- .../Playback/BaseStreamingService.cs | 152 +++++++----------- MediaBrowser.Api/Playback/StreamState.cs | 117 +++++--------- .../MediaEncoding/EncodingJobInfo.cs | 58 +++---- .../MediaEncoding/JobLogger.cs | 7 +- .../Dlna/ContentFeatureBuilder.cs | 20 +-- 6 files changed, 151 insertions(+), 218 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 700cbb9439..a223a4fe38 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -415,7 +415,7 @@ namespace MediaBrowser.Api public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; - //Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); + Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); if (job.ActiveRequestCount <= 0) { PingTimer(job, false); @@ -428,7 +428,7 @@ namespace MediaBrowser.Api throw new ArgumentNullException(nameof(playSessionId)); } - //Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); + Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); List jobs; @@ -443,7 +443,7 @@ namespace MediaBrowser.Api { if (isUserPaused.HasValue) { - //Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id); + Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id); job.IsUserPaused = isUserPaused.Value; } PingTimer(job, true); @@ -601,7 +601,6 @@ namespace MediaBrowser.Api { Logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path); - //process.Kill(); process.StandardInput.WriteLine("q"); // Need to wait because killing is asynchronous @@ -701,7 +700,7 @@ namespace MediaBrowser.Api { try { - //Logger.LogDebug("Deleting HLS file {0}", file); + Logger.LogDebug("Deleting HLS file {0}", file); _fileSystem.DeleteFile(file); } catch (FileNotFoundException) @@ -840,12 +839,12 @@ namespace MediaBrowser.Api { if (KillTimer == null) { - //Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); } else { - //Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer.Change(intervalMs, Timeout.Infinite); } } @@ -864,7 +863,7 @@ namespace MediaBrowser.Api { var intervalMs = PingTimeout; - //Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer.Change(intervalMs, Timeout.Infinite); } } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index ae259a4f59..b480d055af 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -32,6 +30,8 @@ namespace MediaBrowser.Api.Playback /// public abstract class BaseStreamingService : BaseApiService { + protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + /// /// Gets or sets the application paths. /// @@ -65,15 +65,25 @@ namespace MediaBrowser.Api.Playback protected IFileSystem FileSystem { get; private set; } protected IDlnaManager DlnaManager { get; private set; } + protected IDeviceManager DeviceManager { get; private set; } + protected ISubtitleEncoder SubtitleEncoder { get; private set; } + protected IMediaSourceManager MediaSourceManager { get; private set; } + protected IJsonSerializer JsonSerializer { get; private set; } protected IAuthorizationContext AuthorizationContext { get; private set; } protected EncodingHelper EncodingHelper { get; set; } + /// + /// Gets the type of the transcoding job. + /// + /// The type of the transcoding job. + protected abstract TranscodingJobType TranscodingJobType { get; } + /// /// Initializes a new instance of the class. /// @@ -112,12 +122,6 @@ namespace MediaBrowser.Api.Playback /// protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding); - /// - /// Gets the type of the transcoding job. - /// - /// The type of the transcoding job. - protected abstract TranscodingJobType TranscodingJobType { get; } - /// /// Gets the output file extension. /// @@ -133,31 +137,18 @@ namespace MediaBrowser.Api.Playback /// private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension) { - var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - var data = GetCommandLineArguments("dummy\\dummy", encodingOptions, state, false); - data += "-" + (state.Request.DeviceId ?? string.Empty); - data += "-" + (state.Request.PlaySessionId ?? string.Empty); + data += "-" + (state.Request.DeviceId ?? string.Empty) + + "-" + (state.Request.PlaySessionId ?? string.Empty); - var dataHash = data.GetMD5().ToString("N"); + var filename = data.GetMD5().ToString("N") + outputFileExtension.ToLowerInvariant(); + var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - if (EnableOutputInSubFolder) - { - return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant()); - } - - return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant()); + return Path.Combine(folder, filename); } - protected virtual bool EnableOutputInSubFolder => false; - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - protected virtual string GetDefaultH264Preset() - { - return "superfast"; - } + protected virtual string GetDefaultH264Preset() => "superfast"; private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { @@ -171,7 +162,6 @@ namespace MediaBrowser.Api.Playback var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken - }, cancellationTokenSource.Token).ConfigureAwait(false); EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl); @@ -209,22 +199,16 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var auth = AuthorizationContext.GetAuthorizationInfo(Request); - if (auth.User != null) + if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding) { - if (!auth.User.Policy.EnableVideoPlaybackTranscoding) - { - ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); + ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); - throw new ArgumentException("User does not have access to video transcoding"); - } + throw new ArgumentException("User does not have access to video transcoding"); } } var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - var transcodingId = Guid.NewGuid().ToString("N"); - var commandLineArgs = GetCommandLineArguments(outputPath, encodingOptions, state, true); - var process = new Process() { StartInfo = new ProcessStartInfo() @@ -239,7 +223,7 @@ namespace MediaBrowser.Api.Playback RedirectStandardInput = true, FileName = MediaEncoder.EncoderPath, - Arguments = commandLineArgs, + Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true), WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory, ErrorDialog = false @@ -250,7 +234,7 @@ namespace MediaBrowser.Api.Playback var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, state.Request.PlaySessionId, state.MediaSource.LiveStreamId, - transcodingId, + Guid.NewGuid().ToString("N"), TranscodingJobType, process, state.Request.DeviceId, @@ -261,27 +245,26 @@ namespace MediaBrowser.Api.Playback Logger.LogInformation(commandLineLogMessage); var logFilePrefix = "ffmpeg-transcode"; - if (state.VideoRequest != null) + if (state.VideoRequest != null + && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { logFilePrefix = "ffmpeg-directstream"; } - else if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + else { logFilePrefix = "ffmpeg-remux"; } } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); - Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); + Stream logStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); @@ -298,13 +281,10 @@ namespace MediaBrowser.Api.Playback throw; } - // MUST read both stdout and stderr asynchronously or a deadlock may occurr - //process.BeginOutputReadLine(); - state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream); + _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); // Wait for the file to exist before proceeeding while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) @@ -368,25 +348,16 @@ namespace MediaBrowser.Api.Playback Logger.LogDebug("Disposing stream resources"); state.Dispose(); - try + if (process.ExitCode == 0) { - Logger.LogInformation("FFMpeg exited with code {0}", process.ExitCode); + Logger.LogInformation("FFMpeg exited with code 0"); } - catch + else { - Logger.LogError("FFMpeg exited with an error."); + Logger.LogError("FFMpeg exited with code {0}", process.ExitCode); } - // This causes on exited to be called twice: - //try - //{ - // // Dispose the process - // process.Dispose(); - //} - //catch (Exception ex) - //{ - // Logger.LogError(ex, "Error disposing ffmpeg."); - //} + process.Dispose(); } /// @@ -643,11 +614,19 @@ namespace MediaBrowser.Api.Playback return null; } - if (value.IndexOf("npt=", StringComparison.OrdinalIgnoreCase) != 0) + if (!value.StartsWith("npt=", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } - value = value.Substring(4).Split(new[] { '-' }, 2)[0]; + int index = value.IndexOf('-'); + if (index == -1) + { + value = value.Substring(4); + } + else + { + value = value.Substring(4, index); + } if (value.IndexOf(':') == -1) { @@ -728,13 +707,10 @@ namespace MediaBrowser.Api.Playback // state.SegmentLength = 6; //} - if (state.VideoRequest != null) + if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) { - if (!string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) - { - state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); - } + state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(request.AudioCodec)) @@ -779,7 +755,7 @@ namespace MediaBrowser.Api.Playback var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false)).ToList(); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) - ? mediaSources.First() + ? mediaSources[0] : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)); if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) @@ -834,11 +810,11 @@ namespace MediaBrowser.Api.Playback if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var resolution = ResolutionNormalizer.Normalize( - state.VideoStream == null ? (int?)null : state.VideoStream.BitRate, - state.VideoStream == null ? (int?)null : state.VideoStream.Width, - state.VideoStream == null ? (int?)null : state.VideoStream.Height, + state.VideoStream?.BitRate, + state.VideoStream?.Width, + state.VideoStream?.Height, state.OutputVideoBitrate.Value, - state.VideoStream == null ? null : state.VideoStream.Codec, + state.VideoStream?.Codec, state.OutputVideoCodec, videoRequest.MaxWidth, videoRequest.MaxHeight); @@ -846,17 +822,13 @@ namespace MediaBrowser.Api.Playback videoRequest.MaxWidth = resolution.MaxWidth; videoRequest.MaxHeight = resolution.MaxHeight; } + } - ApplyDeviceProfileSettings(state); - } - else - { - ApplyDeviceProfileSettings(state); - } + ApplyDeviceProfileSettings(state); var ext = string.IsNullOrWhiteSpace(state.OutputContainer) ? GetOutputFileExtension(state) - : ("." + state.OutputContainer); + : ('.' + state.OutputContainer); var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); @@ -970,18 +942,18 @@ namespace MediaBrowser.Api.Playback responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode; responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*"; - if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase)) + if (state.RunTimeTicks.HasValue) { - if (state.RunTimeTicks.HasValue) + if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture)); } - } - if (state.RunTimeTicks.HasValue && !isStaticallyStreamed && profile != null) - { - AddTimeSeekResponseHeaders(state, responseHeaders); + if (!isStaticallyStreamed && profile != null) + { + AddTimeSeekResponseHeaders(state, responseHeaders); + } } if (profile == null) diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 8d4b0cb3d0..ba3c2bfbaf 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,9 +1,7 @@ using System; -using System.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback @@ -12,6 +10,7 @@ namespace MediaBrowser.Api.Playback { private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; + private bool _disposed = false; public string RequestedUrl { get; set; } @@ -30,11 +29,6 @@ namespace MediaBrowser.Api.Playback public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; - /// - /// Gets or sets the log file stream. - /// - /// The log file stream. - public Stream LogFileStream { get; set; } public IDirectStreamProvider DirectStreamProvider { get; set; } public string WaitForPath { get; set; } @@ -72,6 +66,7 @@ namespace MediaBrowser.Api.Playback { return 3; } + return 6; } @@ -94,6 +89,16 @@ namespace MediaBrowser.Api.Playback public string UserAgent { get; set; } + public bool EstimateContentLength { get; set; } + + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + public bool EnableDlnaHeaders { get; set; } + + public DeviceProfile DeviceProfile { get; set; } + + public TranscodingJob TranscodingJob { get; set; } + public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType) : base(transcodingType) { @@ -101,75 +106,41 @@ namespace MediaBrowser.Api.Playback _logger = logger; } - public bool EstimateContentLength { get; set; } - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - public bool EnableDlnaHeaders { get; set; } - - public override void Dispose() - { - DisposeTranscodingThrottler(); - DisposeLogStream(); - DisposeLiveStream(); - - TranscodingJob = null; - } - - private void DisposeTranscodingThrottler() - { - if (TranscodingThrottler != null) - { - try - { - TranscodingThrottler.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing TranscodingThrottler"); - } - - TranscodingThrottler = null; - } - } - - private void DisposeLogStream() - { - if (LogFileStream != null) - { - try - { - LogFileStream.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing log stream"); - } - - LogFileStream = null; - } - } - - private async void DisposeLiveStream() - { - if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) - { - try - { - await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing media source"); - } - } - } - - public DeviceProfile DeviceProfile { get; set; } - - public TranscodingJob TranscodingJob; public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) { ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // REVIEW: Is this the right place for this? + if (MediaSource.RequiresClosing + && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) + { + _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); + } + + TranscodingThrottler?.Dispose(); + } + + TranscodingThrottler = null; + TranscodingJob = null; + + _disposed = true; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 916d691b80..34af3b1568 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -374,14 +374,14 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { if (AudioStream != null) { return AudioStream.SampleRate; } } - else if (BaseRequest.AudioSampleRate.HasValue) { // Don't exceed what the encoder supports @@ -397,7 +397,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { if (AudioStream != null) { @@ -405,13 +406,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } - //else if (BaseRequest.AudioSampleRate.HasValue) - //{ - // // Don't exceed what the encoder supports - // // Seeing issues of attempting to encode to 88200 - // return Math.Min(44100, BaseRequest.AudioSampleRate.Value); - //} - return null; } } @@ -446,7 +440,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.BitDepth; } @@ -463,7 +458,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.RefFrames; } @@ -479,7 +475,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); } @@ -545,7 +542,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.CodecTag; } @@ -558,7 +556,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.IsAnamorphic; } @@ -571,14 +570,12 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputVideoCodec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.Codec; } - return codec; + return OutputVideoCodec; } } @@ -586,14 +583,12 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputAudioCodec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return AudioStream?.Codec; } - return codec; + return OutputAudioCodec; } } @@ -601,7 +596,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.IsInterlaced; } @@ -636,6 +632,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Video, 1); } } @@ -648,17 +645,12 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Audio, 1); } } - public int HlsListSize - { - get - { - return 0; - } - } + public int HlsListSize => 0; private int? GetMediaStreamCount(MediaStreamType type, int limit) { @@ -677,10 +669,6 @@ namespace MediaBrowser.Controller.MediaEncoding { Progress.Report(percentComplete.Value); } - - public virtual void Dispose() - { - } } /// diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 2755bf5814..d0d5ebfd69 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; @@ -18,10 +19,11 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) { try { + using (target) using (var reader = new StreamReader(source)) { while (!reader.EndOfStream && reader.BaseStream.CanRead) @@ -97,8 +99,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var currentMs = startMs + val.TotalMilliseconds; - var percentVal = currentMs / totalMs; - percent = 100 * percentVal; + percent = 100 * currentMs / totalMs; transcodingPosition = val; } diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 901d81c5ff..e52951dd08 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -13,7 +13,8 @@ namespace MediaBrowser.Model.Dlna _profile = profile; } - public string BuildImageHeader(string container, + public string BuildImageHeader( + string container, int? width, int? height, bool isDirectStream, @@ -28,8 +29,7 @@ namespace MediaBrowser.Model.Dlna DlnaFlags.InteractiveTransferMode | DlnaFlags.DlnaV15; - string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); + string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container, width, @@ -37,7 +37,7 @@ namespace MediaBrowser.Model.Dlna if (string.IsNullOrEmpty(orgPn)) { - orgPn = mediaProfile == null ? null : mediaProfile.OrgPn; + orgPn = mediaProfile?.OrgPn; } if (string.IsNullOrEmpty(orgPn)) @@ -50,7 +50,8 @@ namespace MediaBrowser.Model.Dlna return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); } - public string BuildAudioHeader(string container, + public string BuildAudioHeader( + string container, string audioCodec, int? audioBitrate, int? audioSampleRate, @@ -102,7 +103,8 @@ namespace MediaBrowser.Model.Dlna return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); } - public List BuildVideoHeader(string container, + public List BuildVideoHeader( + string container, string videoCodec, string audioCodec, int? width, @@ -206,7 +208,7 @@ namespace MediaBrowser.Model.Dlna return contentFeatureList; } - private string GetImageOrgPnValue(string container, int? width, int? height) + private static string GetImageOrgPnValue(string container, int? width, int? height) { MediaFormatProfile? format = new MediaFormatProfileResolver() .ResolveImageFormat(container, @@ -216,7 +218,7 @@ namespace MediaBrowser.Model.Dlna return format.HasValue ? format.Value.ToString() : null; } - private string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) + private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) { MediaFormatProfile? format = new MediaFormatProfileResolver() .ResolveAudioFormat(container, @@ -227,7 +229,7 @@ namespace MediaBrowser.Model.Dlna return format.HasValue ? format.Value.ToString() : null; } - private string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) + private static string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) { return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); } From ca37ca291fb06e8b46257ae1dbc0f2786c948d36 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 26 Mar 2019 23:04:53 +0100 Subject: [PATCH 0276/6208] More style changes --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 6 +++--- MediaBrowser.Api/Playback/StreamState.cs | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index b480d055af..674f3170e4 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -686,7 +686,7 @@ namespace MediaBrowser.Api.Playback var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) /*|| string.Equals(Request.Headers.Get("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase)*/; - var state = new StreamState(MediaSourceManager, Logger, TranscodingJobType) + var state = new StreamState(MediaSourceManager, TranscodingJobType) { Request = request, RequestedUrl = url, @@ -756,11 +756,11 @@ namespace MediaBrowser.Api.Playback mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources[0] - : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)); + : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId)); if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) { - mediaSource = mediaSources.First(); + mediaSource = mediaSources[0]; } } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index ba3c2bfbaf..7396b5c99b 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -2,13 +2,11 @@ using System; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback { public class StreamState : EncodingJobInfo, IDisposable { - private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; private bool _disposed = false; @@ -99,11 +97,10 @@ namespace MediaBrowser.Api.Playback public TranscodingJob TranscodingJob { get; set; } - public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType) + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType) : base(transcodingType) { _mediaSourceManager = mediaSourceManager; - _logger = logger; } public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) From 157a86d0f139d149083036e6ea962e0fd7d77057 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 12:43:46 +0100 Subject: [PATCH 0277/6208] Remove dead code --- .../Services/ServiceHandler.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 243d2cca27..621be4fcb5 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.Services { public class ServiceHandler { - private readonly ServiceController _serviceController; - public RestPath RestPath { get; } public string ResponseContentType { get; } @@ -28,22 +26,12 @@ namespace Emby.Server.Implementations.Services if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) { var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer(requestType, httpReq.InputStream); - } + return deserializer?.Invoke(requestType, httpReq.InputStream); } return Task.FromResult(host.CreateInstance(requestType)); } - public RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) - { - pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); - - return _serviceController.GetRestPathForRequest(httpMethod, pathInfo); - } - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) { contentType = null; From 8ed5d154b746c086ccb6b1163772ea4d05067abd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:07:08 +0100 Subject: [PATCH 0278/6208] Remove duplicate code --- .../Session/SessionManager.cs | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dc23551db5..b7974dd7e8 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1037,6 +1037,25 @@ namespace Emby.Server.Implementations.Session } } + private static Task SendMessageToSessions(IEnumerable sessions, string name, T data, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + foreach (var session in sessions) + { + var controllers = session.SessionControllers; + var messageId = Guid.NewGuid().ToString("N"); + + foreach (var controller in controllers) + { + yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); + } + } + } + + return Task.WhenAll(GetTasks()); + } + public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1832,15 +1851,7 @@ namespace Emby.Server.Implementations.Session var data = dataFn(); - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken) @@ -1848,16 +1859,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken) @@ -1865,16 +1867,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserDeviceAndAdminSessions(string deviceId, string name, T data, CancellationToken cancellationToken) @@ -1883,16 +1876,7 @@ namespace Emby.Server.Implementations.Session var sessions = Sessions .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } private bool IsAdminSession(SessionInfo s) From 6c0e2e249ddbab24c67b655b701e473d9343c2ef Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:13:36 +0100 Subject: [PATCH 0279/6208] Even more duplicate code removed --- .../Session/SessionManager.cs | 56 ++----------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b7974dd7e8..6b9573b1dd 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1242,12 +1242,13 @@ namespace Emby.Server.Implementations.Session return SendMessageToSession(session, "Playstate", command, cancellationToken); } - private void AssertCanControl(SessionInfo session, SessionInfo controllingSession) + private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession) { if (session == null) { throw new ArgumentNullException(nameof(session)); } + if (controllingSession == null) { throw new ArgumentNullException(nameof(controllingSession)); @@ -1259,26 +1260,11 @@ namespace Emby.Server.Implementations.Session /// /// The cancellation token. /// Task. - public async Task SendRestartRequiredNotification(CancellationToken cancellationToken) + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { CheckDisposed(); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "RestartRequired", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendRestartRequiredNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - await Task.WhenAll(tasks).ConfigureAwait(false); + return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken); } /// @@ -1290,22 +1276,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "ServerShuttingDown", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendServerShutdownNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken); } /// @@ -1319,22 +1290,7 @@ namespace Emby.Server.Implementations.Session _logger.LogDebug("Beginning SendServerRestartNotification"); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "ServerRestarting", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendServerRestartNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken); } /// From b647959ec41373c513ee55075e4c973bfb68dbcd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:26:33 +0100 Subject: [PATCH 0280/6208] Add EnableOutputInSubFolder back --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 12 ++++++++++-- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 674f3170e4..cde6f725aa 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -32,6 +32,8 @@ namespace MediaBrowser.Api.Playback { protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + protected virtual bool EnableOutputInSubFolder => false; + /// /// Gets or sets the application paths. /// @@ -142,10 +144,16 @@ namespace MediaBrowser.Api.Playback data += "-" + (state.Request.DeviceId ?? string.Empty) + "-" + (state.Request.PlaySessionId ?? string.Empty); - var filename = data.GetMD5().ToString("N") + outputFileExtension.ToLowerInvariant(); + var filename = data.GetMD5().ToString("N"); + var ext = outputFileExtension.ToLowerInvariant(); var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - return Path.Combine(folder, filename); + if (EnableOutputInSubFolder) + { + return Path.Combine(folder, filename, filename + ext); + } + + return Path.Combine(folder, filename + ext); } protected virtual string GetDefaultH264Preset() => "superfast"; diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index d0d5ebfd69..ac989f6ba1 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var currentMs = startMs + val.TotalMilliseconds; - percent = 100 * currentMs / totalMs; + percent = 100.0 * currentMs / totalMs; transcodingPosition = val; } From b69b19ddce63651306bde494adfa656fd658b230 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:28:52 +0100 Subject: [PATCH 0281/6208] Move messageId out of outer loop --- Emby.Server.Implementations/Session/SessionManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6b9573b1dd..53ed5fc223 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1041,11 +1041,10 @@ namespace Emby.Server.Implementations.Session { IEnumerable GetTasks() { + var messageId = Guid.NewGuid().ToString("N"); foreach (var session in sessions) { var controllers = session.SessionControllers; - var messageId = Guid.NewGuid().ToString("N"); - foreach (var controller in controllers) { yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); From be86ea29828a696aedbbdc21f83ea4222e68b316 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Mar 2019 16:34:56 +0100 Subject: [PATCH 0282/6208] Update MediaBrowser.Common/Net/IHttpClient.cs Co-Authored-By: Bond-009 --- MediaBrowser.Common/Net/IHttpClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index d332ab2000..db69c6f2cd 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Common.Net Task Get(HttpRequestOptions options); /// - /// Warning: Depricated function, + /// Warning: Deprecated function, /// use 'Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead /// Sends the asynchronous. /// From 9aaeb19418616283de00cdb0d00699ab93d84415 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:31:26 +0100 Subject: [PATCH 0283/6208] Self-documenting code --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index cde6f725aa..efbb17fd2a 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -622,18 +622,19 @@ namespace MediaBrowser.Api.Playback return null; } - if (!value.StartsWith("npt=", StringComparison.OrdinalIgnoreCase)) + const string Npt = "npt="; + if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } int index = value.IndexOf('-'); if (index == -1) { - value = value.Substring(4); + value = value.Substring(Npt.Length); } else { - value = value.Substring(4, index); + value = value.Substring(Npt.Length, index); } if (value.IndexOf(':') == -1) From b07c146fd96d9ed7676adffda0333ec85f0c05b6 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 27 Mar 2019 16:17:18 -0700 Subject: [PATCH 0284/6208] Update Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultPasswordResetProvider.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index a589d61689..da65967434 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -40,7 +40,10 @@ namespace Emby.Server.Implementations.Library HashSet usersreset = new HashSet(); foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) { - var spr = await _jsonSerializer.DeserializeFromStreamAsync(File.OpenRead(resetfile)).ConfigureAwait(false); + using (var str = File.OpenRead(resetfile)) + { + var spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } if (spr.ExpirationDate < DateTime.Now) { File.Delete(resetfile); @@ -51,7 +54,7 @@ namespace Emby.Server.Implementations.Library if (resetUser != null) { await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); + usersreset.Add(resetUser.Name); File.Delete(resetfile); } } @@ -85,8 +88,8 @@ namespace Emby.Server.Implementations.Library }; try - { - FileStream fileStream = File.OpenWrite(filePath); + { + FileStream fileStream = File.OpenWrite(filePath); _jsonSerializer.SerializeToStream(spr,fileStream); await fileStream.FlushAsync().ConfigureAwait(false); } From 5e8496bc593399f062169c90b1820c1b8b75a73e Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 27 Mar 2019 22:46:25 -0700 Subject: [PATCH 0285/6208] minor fixes and usings --- .../Library/DefaultPasswordResetProvider.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index da65967434..63ebc7c727 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -10,7 +10,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; -using Microsoft.Win32.SafeHandles; namespace Emby.Server.Implementations.Library { @@ -37,13 +36,15 @@ namespace Emby.Server.Implementations.Library public async Task RedeemPasswordResetPin(string pin) { + SerializablePasswordReset spr; HashSet usersreset = new HashSet(); foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) { using (var str = File.OpenRead(resetfile)) { - var spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } + spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } + if (spr.ExpirationDate < DateTime.Now) { File.Delete(resetfile); @@ -51,12 +52,14 @@ namespace Emby.Server.Implementations.Library else if (spr.Pin == pin) { var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser != null) + if (resetUser == null) { - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - File.Delete(resetfile); + throw new Exception($"User with a username of {spr.UserName} not found"); } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + File.Delete(resetfile); } } @@ -76,7 +79,7 @@ namespace Emby.Server.Implementations.Library public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { - string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); + string pin = new Random().Next(99999999).ToString("00000000", CultureInfo.InvariantCulture); DateTime expireTime = DateTime.Now.AddMinutes(30); string filePath = _passwordResetFileBase + user.InternalId + ".json"; SerializablePasswordReset spr = new SerializablePasswordReset @@ -89,9 +92,11 @@ namespace Emby.Server.Implementations.Library try { - FileStream fileStream = File.OpenWrite(filePath); - _jsonSerializer.SerializeToStream(spr,fileStream); - await fileStream.FlushAsync().ConfigureAwait(false); + using (FileStream fileStream = File.OpenWrite(filePath)) + { + _jsonSerializer.SerializeToStream(spr, fileStream); + await fileStream.FlushAsync().ConfigureAwait(false); + } } catch (Exception e) { From 48b50a22a43dde00c795fb01521fcd731c323de7 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Thu, 28 Mar 2019 08:15:53 -0700 Subject: [PATCH 0286/6208] switched to a hexa string with crypto random backing --- .../Library/DefaultPasswordResetProvider.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 63ebc7c727..b726fa2d0a 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -8,6 +8,7 @@ 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; @@ -25,13 +26,15 @@ namespace Emby.Server.Implementations.Library private IJsonSerializer _jsonSerializer; private IUserManager _userManager; + private ICryptoProvider _crypto; - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); _jsonSerializer = jsonSerializer; _userManager = userManager; + _crypto = cryptoProvider; } public async Task RedeemPasswordResetPin(string pin) @@ -49,7 +52,7 @@ namespace Emby.Server.Implementations.Library { File.Delete(resetfile); } - else if (spr.Pin == pin) + else if (spr.Pin.Equals(pin, StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) @@ -79,7 +82,14 @@ namespace Emby.Server.Implementations.Library public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { - string pin = new Random().Next(99999999).ToString("00000000", CultureInfo.InvariantCulture); + string pin = string.Empty; + using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = bytes.ToString(); + } + DateTime expireTime = DateTime.Now.AddMinutes(30); string filePath = _passwordResetFileBase + user.InternalId + ".json"; SerializablePasswordReset spr = new SerializablePasswordReset From 3001f21f8d8592586add36661a0be0679e427d63 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 28 Mar 2019 19:11:05 +0100 Subject: [PATCH 0287/6208] Hacky fix for a hacky issue --- Emby.Server.Implementations/ApplicationHost.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 1c9a4776a0..ce8483a322 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1042,8 +1042,6 @@ namespace Emby.Server.Implementations CollectionFolder.JsonSerializer = JsonSerializer; CollectionFolder.ApplicationHost = this; AuthenticatedAttribute.AuthService = AuthService; - - InstallationManager.PluginInstalled += PluginInstalled; } private async void PluginInstalled(object sender, GenericEventArgs args) @@ -1085,6 +1083,7 @@ namespace Emby.Server.Implementations protected void FindParts() { InstallationManager = _serviceProvider.GetService(); + InstallationManager.PluginInstalled += PluginInstalled; if (!ServerConfigurationManager.Configuration.IsPortAuthorized) { From 427a3e9b087e57b8308b2c53aaefd663cc3c2a68 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 28 Mar 2019 18:21:25 -0400 Subject: [PATCH 0288/6208] Use new libexecdir location for jellyfin-ffmpeg From commit d6bb1f3c in jellyfin-ffmpeg, which moves the installed binaries from /usr/share to /usr/lib on the next release. --- deployment/debian-package-x64/pkg-src/conf/jellyfin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index bc00c37e20..c6e595f15a 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -22,7 +22,7 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin" JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" # ffmpeg binary paths, overriding the system values -JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg" +JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg" # [OPTIONAL] run Jellyfin as a headless service #JELLYFIN_SERVICE_OPT="--service" From 41df562419d8f1681a9720ab1c62ffb9ad0f96cb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 28 Mar 2019 23:19:56 +0100 Subject: [PATCH 0289/6208] Improve IO code * Style changes * Remove remnants of SMB support * Use `GetInvalidFileNameChars` instead of rolling our own * Remove possible unexpected behaviour with async file streams * Remove some dead code --- .../IO/FileRefresher.cs | 6 +- .../IO/LibraryMonitor.cs | 125 +++--- .../IO/ManagedFileSystem.cs | 68 ++-- .../IO/StreamHelper.cs | 74 +--- .../IO/ThrottledStream.cs | 355 ------------------ 5 files changed, 82 insertions(+), 546 deletions(-) delete mode 100644 Emby.Server.Implementations/IO/ThrottledStream.cs diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 73242d0ade..40e8ed5dc7 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -6,10 +6,6 @@ using System.Threading; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO { AddAffectedPath(path); } + RestartTimer(); } @@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO AddAffectedPath(affectedFile); } } + RestartTimer(); } diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index df4dc41b99..aeb541c540 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.IO { @@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO /// The file system watchers /// private readonly ConcurrentDictionary _fileSystemWatchers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + /// /// The affected paths /// @@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. + // This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata await Task.Delay(45000).ConfigureAwait(false); @@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO public void Start() { - LibraryManager.ItemAdded += LibraryManager_ItemAdded; - LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; + LibraryManager.ItemAdded += OnLibraryManagerItemAdded; + LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved; - var pathsToWatch = new List { }; + var pathsToWatch = new List(); var paths = LibraryManager .RootFolder @@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO return lst.Any(str => { - //this should be a little quicker than examining each actual parent folder... + // this should be a little quicker than examining each actual parent folder... var compare = str.TrimEnd(Path.DirectorySeparatorChar); return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); @@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO if (!Directory.Exists(path)) { // Seeing a crash in the mono runtime due to an exception being thrown on a different thread - Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path); + Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path); return; } - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase)) - { - // not supported - return; - } - } - // Already being watched if (_fileSystemWatchers.ContainsKey(path)) { @@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO { var newWatcher = new FileSystemWatcher(path, "*") { - IncludeSubdirectories = true + IncludeSubdirectories = true, + InternalBufferSize = 65536, + NotifyFilter = NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size | + NotifyFilters.Attributes }; - newWatcher.InternalBufferSize = 65536; - - newWatcher.NotifyFilter = NotifyFilters.CreationTime | - NotifyFilters.DirectoryName | - NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size | - NotifyFilters.Attributes; - - newWatcher.Created += watcher_Changed; - newWatcher.Deleted += watcher_Changed; - newWatcher.Renamed += watcher_Changed; - newWatcher.Changed += watcher_Changed; - newWatcher.Error += watcher_Error; + newWatcher.Created += OnWatcherChanged; + newWatcher.Deleted += OnWatcherChanged; + newWatcher.Renamed += OnWatcherChanged; + newWatcher.Changed += OnWatcherChanged; + newWatcher.Error += OnWatcherError; if (_fileSystemWatchers.TryAdd(path, newWatcher)) { @@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO { using (watcher) { - Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path); + Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path); - watcher.Created -= watcher_Changed; - watcher.Deleted -= watcher_Changed; - watcher.Renamed -= watcher_Changed; - watcher.Changed -= watcher_Changed; - watcher.Error -= watcher_Error; + watcher.Created -= OnWatcherChanged; + watcher.Deleted -= OnWatcherChanged; + watcher.Renamed -= OnWatcherChanged; + watcher.Changed -= OnWatcherChanged; + watcher.Error -= OnWatcherError; - try - { - watcher.EnableRaisingEvents = false; - } - catch (InvalidOperationException) - { - // Seeing this under mono on linux sometimes - // Collection was modified; enumeration operation may not execute. - } + watcher.EnableRaisingEvents = false; } - } - catch (NotImplementedException) - { - // the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android - } - catch - { - } finally { @@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO /// The watcher. private void RemoveWatcherFromList(FileSystemWatcher watcher) { - _fileSystemWatchers.TryRemove(watcher.Path, out var removed); + _fileSystemWatchers.TryRemove(watcher.Path, out _); } /// @@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void watcher_Error(object sender, ErrorEventArgs e) + private void OnWatcherError(object sender, ErrorEventArgs e) { var ex = e.GetException(); var dw = (FileSystemWatcher)sender; - Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path); + Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); } @@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void watcher_Changed(object sender, FileSystemEventArgs e) + private void OnWatcherChanged(object sender, FileSystemEventArgs e) { try { - //logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); - - var path = e.FullPath; - - ReportFileSystemChanged(path); + ReportFileSystemChanged(e.FullPath); } catch (Exception ex) { @@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO { if (_fileSystem.AreEqual(i, path)) { - Logger.LogDebug("Ignoring change to {path}", path); + Logger.LogDebug("Ignoring change to {Path}", path); return true; } if (_fileSystem.ContainsSubPath(i, path)) { - Logger.LogDebug("Ignoring change to {path}", path); + Logger.LogDebug("Ignoring change to {Path}", path); return true; } // Go up a level var parent = Path.GetDirectoryName(i); - if (!string.IsNullOrEmpty(parent)) + if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) { - if (_fileSystem.AreEqual(parent, path)) - { - Logger.LogDebug("Ignoring change to {path}", path); - return true; - } + Logger.LogDebug("Ignoring change to {Path}", path); + return true; } return false; @@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO lock (_activeRefreshers) { - var refreshers = _activeRefreshers.ToList(); - foreach (var refresher in refreshers) + foreach (var refresher in _activeRefreshers) { // Path is already being refreshed if (_fileSystem.AreEqual(path, refresher.Path)) @@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO /// public void Stop() { - LibraryManager.ItemAdded -= LibraryManager_ItemAdded; - LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; + LibraryManager.ItemAdded -= OnLibraryManagerItemAdded; + LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved; foreach (var watcher in _fileSystemWatchers.Values.ToList()) { @@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO { refresher.Dispose(); } + _activeRefreshers.Clear(); } } - private bool _disposed; + private bool _disposed = false; + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 47cea7269d..4b5cfe3b9f 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -19,8 +19,6 @@ namespace Emby.Server.Implementations.IO { protected ILogger Logger; - private readonly bool _supportsAsyncFileStreams; - private char[] _invalidFileNameChars; private readonly List _shortcutHandlers = new List(); private readonly string _tempPath; @@ -32,11 +30,8 @@ namespace Emby.Server.Implementations.IO IApplicationPaths applicationPaths) { Logger = loggerFactory.CreateLogger("FileSystem"); - _supportsAsyncFileStreams = true; _tempPath = applicationPaths.TempDirectory; - SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows); - _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; } @@ -45,20 +40,6 @@ namespace Emby.Server.Implementations.IO _shortcutHandlers.Add(handler); } - protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars) - { - if (enableManagedInvalidFileNameChars) - { - _invalidFileNameChars = Path.GetInvalidFileNameChars(); - } - else - { - // Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions - // https://referencesource.microsoft.com/#mscorlib/system/io/path.cs - _invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' }; - } - } - /// /// Determines whether the specified filename is shortcut. /// @@ -92,20 +73,25 @@ namespace Emby.Server.Implementations.IO var extension = Path.GetExtension(filename); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); - if (handler != null) - { - return handler.Resolve(filename); - } - - return null; + return handler?.Resolve(filename); } public virtual string MakeAbsolutePath(string folderPath, string filePath) { - if (string.IsNullOrWhiteSpace(filePath)) return filePath; + if (string.IsNullOrWhiteSpace(filePath)) + { + return filePath; + } - if (filePath.Contains(@"://")) return filePath; //stream - if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path + if (filePath.Contains("://")) + { + return filePath; // stream + } + + if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') + { + return filePath; // absolute local path + } // unc path if (filePath.StartsWith("\\\\")) @@ -125,9 +111,7 @@ namespace Emby.Server.Implementations.IO } try { - string path = System.IO.Path.Combine(folderPath, filePath); - path = System.IO.Path.GetFullPath(path); - return path; + return Path.Combine(Path.GetFullPath(folderPath), filePath); } catch (ArgumentException) { @@ -166,7 +150,7 @@ namespace Emby.Server.Implementations.IO } var extension = Path.GetExtension(shortcutPath); - var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); if (handler != null) { @@ -244,12 +228,13 @@ namespace Emby.Server.Implementations.IO private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) { - var result = new FileSystemMetadata(); - - result.Exists = info.Exists; - result.FullName = info.FullName; - result.Extension = info.Extension; - result.Name = info.Name; + var result = new FileSystemMetadata + { + Exists = info.Exists, + FullName = info.FullName, + Extension = info.Extension, + Name = info.Name + }; if (result.Exists) { @@ -260,8 +245,7 @@ namespace Emby.Server.Implementations.IO // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; //} - var fileInfo = info as FileInfo; - if (fileInfo != null) + if (info is FileInfo fileInfo) { result.Length = fileInfo.Length; result.DirectoryName = fileInfo.DirectoryName; @@ -307,7 +291,7 @@ namespace Emby.Server.Implementations.IO { var builder = new StringBuilder(filename); - foreach (var c in _invalidFileNameChars) + foreach (var c in Path.GetInvalidFileNameChars()) { builder = builder.Replace(c, ' '); } @@ -394,7 +378,7 @@ namespace Emby.Server.Implementations.IO /// FileStream. public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) { - if (_supportsAsyncFileStreams && isAsync) + if (isAsync) { return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous); } diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index d02cd84a03..7c8c079e39 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO try { int read; - while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); if (onStarted != null) { @@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO if (emptyReadLimit <= 0) { int read; - while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); } return; @@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO { cancellationToken.ThrowIfCancellationRequested(); - var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO { eofCount = 0; - await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); } } } @@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO } } - public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); - try - { - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - } - - return totalBytesRead; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); - try - { - int bytesRead; - - while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); @@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO if (bytesRead == 0) { - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } } @@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { - await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; } diff --git a/Emby.Server.Implementations/IO/ThrottledStream.cs b/Emby.Server.Implementations/IO/ThrottledStream.cs deleted file mode 100644 index 81e8abc983..0000000000 --- a/Emby.Server.Implementations/IO/ThrottledStream.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.IO -{ - /// - /// Class for streaming data with throttling support. - /// - public class ThrottledStream : Stream - { - /// - /// A constant used to specify an infinite number of bytes that can be transferred per second. - /// - public const long Infinite = 0; - - #region Private members - /// - /// The base stream. - /// - private readonly Stream _baseStream; - - /// - /// The maximum bytes per second that can be transferred through the base stream. - /// - private long _maximumBytesPerSecond; - - /// - /// The number of bytes that has been transferred since the last throttle. - /// - private long _byteCount; - - /// - /// The start time in milliseconds of the last throttle. - /// - private long _start; - #endregion - - #region Properties - /// - /// Gets the current milliseconds. - /// - /// The current milliseconds. - protected long CurrentMilliseconds => Environment.TickCount; - - /// - /// Gets or sets the maximum bytes per second that can be transferred through the base stream. - /// - /// The maximum bytes per second. - public long MaximumBytesPerSecond - { - get => _maximumBytesPerSecond; - set - { - if (MaximumBytesPerSecond != value) - { - _maximumBytesPerSecond = value; - Reset(); - } - } - } - - /// - /// Gets a value indicating whether the current stream supports reading. - /// - /// true if the stream supports reading; otherwise, false. - public override bool CanRead => _baseStream.CanRead; - - /// - /// Gets a value indicating whether the current stream supports seeking. - /// - /// - /// true if the stream supports seeking; otherwise, false. - public override bool CanSeek => _baseStream.CanSeek; - - /// - /// Gets a value indicating whether the current stream supports writing. - /// - /// - /// true if the stream supports writing; otherwise, false. - public override bool CanWrite => _baseStream.CanWrite; - - /// - /// Gets the length in bytes of the stream. - /// - /// - /// A long value representing the length of the stream in bytes. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Length => _baseStream.Length; - - /// - /// Gets or sets the position within the current stream. - /// - /// - /// The current position within the stream. - /// An I/O error occurs. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Position - { - get => _baseStream.Position; - set => _baseStream.Position = value; - } - #endregion - - public long MinThrottlePosition; - - #region Ctor - /// - /// Initializes a new instance of the class. - /// - /// The base stream. - /// The maximum bytes per second that can be transferred through the base stream. - /// Thrown when is a null reference. - /// Thrown when is a negative value. - public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) - { - if (baseStream == null) - { - throw new ArgumentNullException(nameof(baseStream)); - } - - if (maximumBytesPerSecond < 0) - { - throw new ArgumentOutOfRangeException(nameof(maximumBytesPerSecond), - maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); - } - - _baseStream = baseStream; - _maximumBytesPerSecond = maximumBytesPerSecond; - _start = CurrentMilliseconds; - _byteCount = 0; - } - #endregion - - #region Public methods - /// - /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. - /// - /// An I/O error occurs. - public override void Flush() - { - _baseStream.Flush(); - } - - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. - /// - /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. - /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. - /// The maximum number of bytes to be read from the current stream. - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. - /// - /// The sum of offset and count is larger than the buffer length. - /// Methods were called after the stream was closed. - /// The base stream does not support reading. - /// buffer is null. - /// An I/O error occurs. - /// offset or count is negative. - public override int Read(byte[] buffer, int offset, int count) - { - Throttle(count); - - return _baseStream.Read(buffer, offset, count); - } - - /// - /// Sets the position within the current stream. - /// - /// A byte offset relative to the origin parameter. - /// A value of type indicating the reference point used to obtain the new position. - /// - /// The new position within the current stream. - /// - /// An I/O error occurs. - /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. - /// Methods were called after the stream was closed. - public override long Seek(long offset, SeekOrigin origin) - { - return _baseStream.Seek(offset, origin); - } - - /// - /// Sets the length of the current stream. - /// - /// The desired length of the current stream in bytes. - /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. - /// An I/O error occurs. - /// Methods were called after the stream was closed. - public override void SetLength(long value) - { - _baseStream.SetLength(value); - } - - private long _bytesWritten; - - /// - /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. - /// - /// An array of bytes. This method copies count bytes from buffer to the current stream. - /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. - /// The number of bytes to be written to the current stream. - /// An I/O error occurs. - /// The base stream does not support writing. - /// Methods were called after the stream was closed. - /// buffer is null. - /// The sum of offset and count is greater than the buffer length. - /// offset or count is negative. - public override void Write(byte[] buffer, int offset, int count) - { - Throttle(count); - - _baseStream.Write(buffer, offset, count); - - _bytesWritten += count; - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); - - await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - _bytesWritten += count; - } - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return _baseStream.ToString(); - } - #endregion - - private bool ThrottleCheck(int bufferSizeInBytes) - { - if (_bytesWritten < MinThrottlePosition) - { - return false; - } - - // Make sure the buffer isn't empty. - if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) - { - return false; - } - - return true; - } - - #region Protected methods - /// - /// Throttles for the specified buffer size in bytes. - /// - /// The buffer size in bytes. - protected void Throttle(int bufferSizeInBytes) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - try - { - // The time to sleep is more then a millisecond, so sleep. - var task = Task.Delay(toSleep); - Task.WaitAll(task); - } - catch - { - // Eatup ThreadAbortException. - } - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - // The time to sleep is more then a millisecond, so sleep. - await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - /// - /// Will reset the bytecount to 0 and reset the start time to the current time. - /// - protected void Reset() - { - long difference = CurrentMilliseconds - _start; - - // Only reset counters when a known history is available of more then 1 second. - if (difference > 1000) - { - _byteCount = 0; - _start = CurrentMilliseconds; - } - } - #endregion - } -} From b56031b9f3ccfd4a8ac0413657f45645fe2e0f1e Mon Sep 17 00:00:00 2001 From: Phallacy Date: Thu, 28 Mar 2019 20:49:11 -0700 Subject: [PATCH 0290/6208] fix byte string --- .../Library/DefaultPasswordResetProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index b726fa2d0a..56540cc089 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; @@ -87,7 +88,7 @@ namespace Emby.Server.Implementations.Library { byte[] bytes = new byte[4]; cryptoRandom.GetBytes(bytes); - pin = bytes.ToString(); + pin = BitConverter.ToString(bytes); } DateTime expireTime = DateTime.Now.AddMinutes(30); From 2d396cb589722bf8a950f80abb6d6137fe084a52 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Fri, 29 Mar 2019 07:10:49 -0700 Subject: [PATCH 0291/6208] adds readonly to properties --- .../Library/DefaultPasswordResetProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 56540cc089..256399d2f0 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -25,9 +25,9 @@ namespace Emby.Server.Implementations.Library private readonly string _passwordResetFileBaseDir; private readonly string _passwordResetFileBaseName = "passwordreset"; - private IJsonSerializer _jsonSerializer; - private IUserManager _userManager; - private ICryptoProvider _crypto; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + private readonly ICryptoProvider _crypto; public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) { From f911fda34fdc1af76dc475c5af042ff6b44262ab Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 29 Mar 2019 20:34:42 +0100 Subject: [PATCH 0292/6208] Merge ifs --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 4b5cfe3b9f..0dea5041a1 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -78,16 +78,13 @@ namespace Emby.Server.Implementations.IO public virtual string MakeAbsolutePath(string folderPath, string filePath) { - if (string.IsNullOrWhiteSpace(filePath)) + if (string.IsNullOrWhiteSpace(filePath) + // stream + || filePath.Contains("://")) { return filePath; } - if (filePath.Contains("://")) - { - return filePath; // stream - } - if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') { return filePath; // absolute local path From 13e94a8b1b78d570a528eee65ff777412f0e83c8 Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Fri, 29 Mar 2019 12:48:07 -0700 Subject: [PATCH 0293/6208] Remove dashes from pins --- .../Library/DefaultPasswordResetProvider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 256399d2f0..c6d4755208 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -47,13 +47,13 @@ namespace Emby.Server.Implementations.Library using (var str = File.OpenRead(resetfile)) { spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } + } if (spr.ExpirationDate < DateTime.Now) { File.Delete(resetfile); } - else if (spr.Pin.Equals(pin, StringComparison.InvariantCultureIgnoreCase)) + else if (spr.Pin.Replace('-', '').Equals(pin.Replace('-', ''), StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) @@ -85,11 +85,11 @@ namespace Emby.Server.Implementations.Library { string pin = string.Empty; using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) - { + { byte[] bytes = new byte[4]; cryptoRandom.GetBytes(bytes); pin = BitConverter.ToString(bytes); - } + } DateTime expireTime = DateTime.Now.AddMinutes(30); string filePath = _passwordResetFileBase + user.InternalId + ".json"; From f0fbd0232cd2367dda26f3f895926c1d0f742bdd Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 29 Mar 2019 19:13:01 -0400 Subject: [PATCH 0294/6208] Correct bad quote characters --- .../Library/DefaultPasswordResetProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index c6d4755208..e218749d90 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library { File.Delete(resetfile); } - else if (spr.Pin.Replace('-', '').Equals(pin.Replace('-', ''), StringComparison.InvariantCultureIgnoreCase)) + else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) From 1a540f1cf7c0f585fd80bc28d12caa56778c878c Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 11:50:46 -0400 Subject: [PATCH 0295/6208] Add Ubuntu armhf (Raspberry Pi) build A pretty-much direct copy of the Debian armhf build infrastructure. --- build.yaml | 1 + .../ubuntu-package-armhf/Dockerfile.amd64 | 42 +++++++++++++++++++ .../ubuntu-package-armhf/Dockerfile.armhf | 34 +++++++++++++++ deployment/ubuntu-package-armhf/clean.sh | 29 +++++++++++++ .../ubuntu-package-armhf/dependencies.txt | 1 + .../ubuntu-package-armhf/docker-build.sh | 20 +++++++++ deployment/ubuntu-package-armhf/package.sh | 42 +++++++++++++++++++ deployment/ubuntu-package-armhf/pkg-src | 1 + 8 files changed, 170 insertions(+) create mode 100644 deployment/ubuntu-package-armhf/Dockerfile.amd64 create mode 100644 deployment/ubuntu-package-armhf/Dockerfile.armhf create mode 100755 deployment/ubuntu-package-armhf/clean.sh create mode 100644 deployment/ubuntu-package-armhf/dependencies.txt create mode 100755 deployment/ubuntu-package-armhf/docker-build.sh create mode 100755 deployment/ubuntu-package-armhf/package.sh create mode 120000 deployment/ubuntu-package-armhf/pkg-src diff --git a/build.yaml b/build.yaml index b0d2502d53..289c1caddc 100644 --- a/build.yaml +++ b/build.yaml @@ -6,6 +6,7 @@ packages: - debian-package-x64 - debian-package-armhf - ubuntu-package-x64 + - ubuntu-package-armhf - fedora-package-x64 - centos-package-x64 - linux-x64 diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64 new file mode 100644 index 0000000000..3e2f9defca --- /dev/null +++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64 @@ -0,0 +1,42 @@ +FROM ubuntu:bionic +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Prepare the cross-toolchain +RUN dpkg --add-architecture armhf \ + && apt-get update \ + && apt-get install -y cross-gcc-dev \ + && TARGET_LIST="armhf" cross-gcc-gensource 6 \ + && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ + && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf new file mode 100644 index 0000000000..72c4647241 --- /dev/null +++ b/deployment/ubuntu-package-armhf/Dockerfile.armhf @@ -0,0 +1,34 @@ +FROM ubuntu:bionic +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=armhf + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-armhf/clean.sh b/deployment/ubuntu-package-armhf/clean.sh new file mode 100755 index 0000000000..c92c7fdec6 --- /dev/null +++ b/deployment/ubuntu-package-armhf/clean.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +keep_artifacts="${1}" + +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-ubuntu-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/ubuntu-package-armhf/dependencies.txt b/deployment/ubuntu-package-armhf/dependencies.txt new file mode 100644 index 0000000000..bdb9670965 --- /dev/null +++ b/deployment/ubuntu-package-armhf/dependencies.txt @@ -0,0 +1 @@ +docker diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh new file mode 100755 index 0000000000..45e68f0c6b --- /dev/null +++ b/deployment/ubuntu-package-armhf/docker-build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Builds the DEB inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image +sed -i '/dotnet-sdk-2.2,/d' debian/control + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -aarmhf + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/deb +mv /jellyfin_* ${ARTIFACT_DIR}/deb/ diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh new file mode 100755 index 0000000000..fb03652cd5 --- /dev/null +++ b/deployment/ubuntu-package-armhf/package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +ARCH="$( arch )" +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-ubuntu_armhf-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Determine which Dockerfile to use +case $ARCH in + 'x86_64') + DOCKERFILE="Dockerfile.amd64" + ;; + 'armv7l') + DOCKERFILE="Dockerfile.armhf" + ;; +esac + +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +# Correct ownership on the DEBs (as current user, then as root if that fails) +chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ + || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-armhf/pkg-src b/deployment/ubuntu-package-armhf/pkg-src new file mode 120000 index 0000000000..4c695fea17 --- /dev/null +++ b/deployment/ubuntu-package-armhf/pkg-src @@ -0,0 +1 @@ +../debian-package-x64/pkg-src \ No newline at end of file From 1596e93cc12cb7e2a1c974eb36b22e835036f582 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 11:57:10 -0400 Subject: [PATCH 0296/6208] Fix up the Ubuntu repository definitions --- deployment/ubuntu-package-armhf/Dockerfile.amd64 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64 index 3e2f9defca..c7575bacaa 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.amd64 +++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64 @@ -22,11 +22,16 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4 && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet # Prepare the cross-toolchain -RUN dpkg --add-architecture armhf \ +RUN rm /etc/apt/sources.list \ + && export CODENAME="$( lsb_release -c -s )" \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >/etc/apt/sources.list.d/armhf.list \ + && dpkg --add-architecture armhf \ && apt-get update \ && apt-get install -y cross-gcc-dev \ && TARGET_LIST="armhf" cross-gcc-gensource 6 \ && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ + && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf # Link to docker-build script From 3375ca5a8cfad159e61f4d8dcd91df26d3c1e71e Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 12:19:49 -0400 Subject: [PATCH 0297/6208] Split lists echoes into separate lines --- deployment/ubuntu-package-armhf/Dockerfile.amd64 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64 index c7575bacaa..ef0735e427 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.amd64 +++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64 @@ -24,8 +24,14 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4 # Prepare the cross-toolchain RUN rm /etc/apt/sources.list \ && export CODENAME="$( lsb_release -c -s )" \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse\ndeb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ && dpkg --add-architecture armhf \ && apt-get update \ && apt-get install -y cross-gcc-dev \ From 1d9133a5e871c84ca5cea5a7608c34378dc6663f Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 12:42:16 -0400 Subject: [PATCH 0298/6208] Simplify bump_version and remove changelogs Make this a lot simpler, use a reference to the release page in the package changelogs instead of a full list. --- bump_version | 133 +++++++++++---------------------------------------- 1 file changed, 27 insertions(+), 106 deletions(-) diff --git a/bump_version b/bump_version index a63fbf7353..d2560ff1e2 100755 --- a/bump_version +++ b/bump_version @@ -9,7 +9,7 @@ usage() { echo -e "bump_version - increase the shared version and generate changelogs" echo -e "" echo -e "Usage:" - echo -e " $ bump_version [-b/--web-branch ] " + echo -e " $ bump_version " echo -e "" echo -e "The web_branch defaults to the same branch name as the current main branch." echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z" @@ -22,14 +22,9 @@ if [[ -z $1 ]]; then fi shared_version_file="./SharedVersion.cs" +build_file="./build.yaml" -# Parse branch option -if [[ $1 == '-b' || $1 == '--web-branch' ]]; then - web_branch="$2" - shift 2 -else - web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )" -fi +web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )" # Initialize submodules git submodule update --init --recursive @@ -47,22 +42,11 @@ if ! git diff-index --quiet HEAD --; then fi git fetch --all -# If this is an official branch name, fetch it from origin -official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$" -if [[ ${web_branch} =~ ${official_branches_regex} ]]; then - git checkout origin/${web_branch} || { - echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid." - exit 1 - } -# Otherwise, just check out the local branch (for testing, etc.) -else - git checkout ${web_branch} || { - echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid." - exit 1 - } -fi +git checkout origin/${web_branch} popd +git add MediaBrowser.WebDashboard/jellyfin-web + new_version="$1" # Parse the version from the AssemblyVersion @@ -73,91 +57,32 @@ old_version="$( # Set the shared version to the specified new_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars -sed -i "s/${old_version_sed}/${new_version}/g" ${shared_version_file} +new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" +sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file} -declare -a pr_merges_since_last_master -declare changelog_string_github -declare changelog_string_deb -declare changelog_string_yum +old_version="$( + grep "version:" ${build_file} \ + | sed -E 's/version: "([0-9\.]+)"/\1/' +)" -# Build up a changelog from merge commits -for repo in ./ MediaBrowser.WebDashboard/jellyfin-web/; do - last_master_merge_commit="" - pr_merges_since_last_master=() - git_show_details="" - pull_request_id="" - pull_request_description="" - changelog_strings_repo_github="" - changelog_strings_repo_deb="" - changelog_strings_repo_yum="" +old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars +new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" +sed -i "s/${old_version_sed}/${new_version}/g" ${build_file} - case $repo in - *jellyfin-web*) - repo_name="jellyfin-web" - ;; - *) - repo_name="jellyfin" - ;; - esac - - pushd ${repo} - - # Find the last release commit, so we know what's happened since - last_master_branch="release-${old_version}" - last_master_merge_commit="$( - git log --merges --pretty=oneline \ - | grep -F "${last_master_branch}" \ - | awk '{ print $1 }' \ - || true # Don't die here with errexit - )" - if [[ -z ${last_master_merge_commit} ]]; then - # This repo has no last proper commit, so just skip it - popd - continue - fi - # Get all the PR merge commits since the last master merge commit in `jellyfin` - pr_merges_since_last_master+=( $( - git log --merges --pretty=oneline ${last_master_merge_commit}..HEAD \ - | grep -F "Merge pull request" \ - | awk '{ print $1 }' - ) ) - - for commit_hash in ${pr_merges_since_last_master[@]}; do - git_show_details="$( git show ${commit_hash} )" - pull_request_id="$( - awk ' - /Merge pull request/{ print $4 } - { next } - ' <<<"${git_show_details}" - )" - pull_request_description="$( - awk ' - /^[a-zA-Z]/{ next } - /^ Merge/{ next } - /^$/{ next } - { print $0 } - ' <<<"${git_show_details}" - )" - pull_request_description="$( sed ':a;N;$!ba;s/\n//g; s/ \+//g' <<<"${pull_request_description}" )" - changelog_strings_repo_github="${changelog_strings_repo_github}\n* ${pull_request_id}: ${pull_request_description}" - changelog_strings_repo_deb="${changelog_strings_repo_deb}\n * $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}" - changelog_strings_repo_yum="${changelog_strings_repo_yum}\n- $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}" - done - - changelog_string_github="${changelog_string_github}\n#### ${repo_name}:\n$( echo -e "${changelog_strings_repo_github}" | sort -nk2 )\n" - changelog_string_deb="${changelog_string_deb}\n * ${repo_name}:$( echo -e "${changelog_strings_repo_deb}" | sort -nk2 )" - changelog_string_yum="${changelog_string_yum}\n- ${repo_name}:$( echo -e "${changelog_strings_repo_yum}" | sort -nk2 )" - - popd -done +if [[ ${new_version} == *"-"* ]]; then + new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )" +else + new_version_deb="${new_version}-1" +fi # Write out a temporary Debian changelog with our new stuff appended and some templated formatting debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog" debian_changelog_temp="$( mktemp )" # Create new temp file with our changelog echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. -jellyfin (${new_version}-1) unstable; urgency=medium -${changelog_string_deb} +jellyfin (${new_version_deb}) unstable; urgency=medium + + * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version} -- Jellyfin Packaging Team $( date --rfc-2822 ) " >> ${debian_changelog_temp} @@ -180,13 +105,14 @@ pushd ${fedora_spec_temp_dir} # Split out the stuff before and after changelog csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01 # Update the version in xx00 -sed -i "s/${old_version_sed}/${new_version}/g" xx00 +sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00 # Remove the header from xx01 sed -i '/^%changelog/d' xx01 # Create new temp file with our changelog echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. %changelog -* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team ${changelog_string_yum}" >> ${fedora_changelog_temp} +* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team +- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp} cat xx01 >> ${fedora_changelog_temp} # Edit the file to verify $EDITOR ${fedora_changelog_temp} @@ -199,10 +125,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file} rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir} # Stage the changed files for commit -git add ${shared_version_file} ${debian_changelog_file} ${fedora_spec_file} +git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} git status - -# Write out the GitHub-formatted changelog for the merge request/release pages -echo "" -echo "=== The GitHub-formatted changelog follows ===" -echo -e "${changelog_string_github}" From 31aa6c486cdeb851dd7ef7c66a82a76302cc632f Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 12:42:33 -0400 Subject: [PATCH 0299/6208] Get the version string from build.yaml For the purposes of packaging, this makes more sense, since we can include additional appends to this version (e.g. `-rcX`) when we can't in the SharedVersion file. The previous commit to the bump_version script sets this as well. --- deployment/common.build.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/common.build.sh b/deployment/common.build.sh index d028e3a668..000872ea91 100755 --- a/deployment/common.build.sh +++ b/deployment/common.build.sh @@ -17,12 +17,12 @@ DEFAULT_PKG_DIR="pkg-dist" DEFAULT_DOCKERFILE="Dockerfile" DEFAULT_ARCHIVE_CMD="tar -xvzf" -# Parse the version from the AssemblyVersion +# Parse the version from the build.yaml version get_version() ( local ROOT=${1-$DEFAULT_ROOT} - grep "AssemblyVersion" ${ROOT}/SharedVersion.cs \ - | sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/' + grep "version:" ${ROOT}/build.yaml \ + | sed -E 's/version: "([0-9\.]+.*)"/\1/' ) # Run a build From 891a03c038e8ef451d6db3361fefcbf964a7065e Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 12:58:39 -0400 Subject: [PATCH 0300/6208] Remove superfluous variable declaration --- bump_version | 1 - 1 file changed, 1 deletion(-) diff --git a/bump_version b/bump_version index d2560ff1e2..b118af54b9 100755 --- a/bump_version +++ b/bump_version @@ -66,7 +66,6 @@ old_version="$( )" old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars -new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" sed -i "s/${old_version_sed}/${new_version}/g" ${build_file} if [[ ${new_version} == *"-"* ]]; then From f27477da26097ab4cca22c64e5f236150975ff47 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 30 Mar 2019 15:47:34 -0400 Subject: [PATCH 0301/6208] Bump version to 10.3.0 and update submodule --- MediaBrowser.WebDashboard/jellyfin-web | 2 +- SharedVersion.cs | 4 ++-- build.yaml | 2 +- deployment/debian-package-x64/pkg-src/changelog | 6 ++++++ deployment/fedora-package-x64/pkg-src/jellyfin.spec | 4 +++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index ec5a3b6e5e..9677981344 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d +Subproject commit 9677981344e57e8f84ca664cad13da1f89f4254f diff --git a/SharedVersion.cs b/SharedVersion.cs index 785ba93018..3a0263bd67 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.2.2")] -[assembly: AssemblyFileVersion("10.2.2")] +[assembly: AssemblyVersion("10.3.0")] +[assembly: AssemblyFileVersion("10.3.0")] diff --git a/build.yaml b/build.yaml index 289c1caddc..34d356dacf 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.2.2" +version: "10.3.0-rc1" packages: - debian-package-x64 - debian-package-armhf diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 349e8787f6..da9151af1a 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,9 @@ +jellyfin (10.3.0~rc1) unstable; urgency=medium + + * New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 + + -- Jellyfin Packaging Team Sat, 30 Mar 2019 15:47:24 -0400 + jellyfin (10.2.2-1) unstable; urgency=medium * jellyfin: diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index e24bd2fcb1..77c291c872 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,7 +7,7 @@ %endif Name: jellyfin -Version: 10.2.2 +Version: 10.3.0 Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 @@ -140,6 +140,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Sat Mar 30 2019 Jellyfin Packaging Team +- New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 * Thu Feb 28 2019 Jellyfin Packaging Team - jellyfin: - PR968 Release 10.2.z copr autobuild From f2e2065fd4a879c8583f487e161e09909ed704b2 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 31 Mar 2019 15:24:18 +0200 Subject: [PATCH 0302/6208] Remove unused dependency for Emby.Naming --- Emby.Naming/Emby.Naming.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index e344e7811e..c448ec0ce6 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -10,7 +10,7 @@ - + From e37ccd6ec0c5ac58cc2cc39e53cd7fb268ad04de Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Sun, 31 Mar 2019 10:32:56 -0700 Subject: [PATCH 0303/6208] Updates windows installer default lib location You can use the emby import to move an existing library this way. --- deployment/windows/install-jellyfin.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/windows/install-jellyfin.ps1 b/deployment/windows/install-jellyfin.ps1 index b6e00e0568..689dedb4a9 100644 --- a/deployment/windows/install-jellyfin.ps1 +++ b/deployment/windows/install-jellyfin.ps1 @@ -58,7 +58,7 @@ function Elevate-Window { if($Quiet.IsPresent -or $Quiet -eq $true){ if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){ - $Script:JellyfinDataDir = "$env:AppData\jellyfin\" + $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\" }else{ $Script:JellyfinDataDir = $JellyfinLibraryLocation } @@ -82,7 +82,7 @@ if($Quiet.IsPresent -or $Quiet -eq $true){ }else{ $Script:InstallServiceAsUser = $true $Script:UserCredentials = $ServiceUser - $Script:JellyfinDataDir = "C:\Users\$($Script:UserCredentials.UserName)\Appdata\Roaming\jellyfin\"} + $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Roaming\jellyfin\"} if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false} if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false} if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false} @@ -131,7 +131,7 @@ if($Quiet.IsPresent -or $Quiet -eq $true){ Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Application]::EnableVisualStyles() -$Script:JellyFinDataDir = "$env:AppData\jellyfin\" +$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\" $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\" $Script:InstallAsService = $False @@ -392,7 +392,7 @@ $ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDow $GUIElementsCollection += $ServiceUserBox $MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox -$MigrateLibraryCheck.text = "Import Emby Library" +$MigrateLibraryCheck.text = "Import Emby/Old JF Library" $MigrateLibraryCheck.AutoSize = $false $MigrateLibraryCheck.width = 160 $MigrateLibraryCheck.height = 20 @@ -401,7 +401,7 @@ $MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10' $GUIElementsCollection += $MigrateLibraryCheck $LibraryMigrationLabel = New-Object system.Windows.Forms.Label -$LibraryMigrationLabel.text = "Emby Library Path" +$LibraryMigrationLabel.text = "Emby/Old JF Library Path" $LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $LibraryMigrationLabel.AutoSize = $false $LibraryMigrationLabel.width = 120 From 816d8a0216989497f6c43da7b9f64539e63bc5bf Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Sun, 31 Mar 2019 10:34:49 -0700 Subject: [PATCH 0304/6208] Update install-jellyfin.ps1 --- deployment/windows/install-jellyfin.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/windows/install-jellyfin.ps1 b/deployment/windows/install-jellyfin.ps1 index 689dedb4a9..0cd7f5236f 100644 --- a/deployment/windows/install-jellyfin.ps1 +++ b/deployment/windows/install-jellyfin.ps1 @@ -82,7 +82,7 @@ if($Quiet.IsPresent -or $Quiet -eq $true){ }else{ $Script:InstallServiceAsUser = $true $Script:UserCredentials = $ServiceUser - $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Roaming\jellyfin\"} + $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"} if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false} if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false} if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false} From 2f33e99006fd479ac9134ede80cbb990948d1261 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 2 Apr 2019 18:17:50 +0200 Subject: [PATCH 0305/6208] Speed up DeepCopy --- .../Entities/BaseItemExtensions.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 9c955a7247..815239be24 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -64,21 +64,31 @@ namespace MediaBrowser.Controller.Entities where T : BaseItem where TU : BaseItem { - var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList(); - var destProps = typeof(TU).GetProperties() - .Where(x => x.CanWrite) - .ToList(); + var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList(); - foreach (var sourceProp in sourceProps) + foreach (var sourceProp in typeof(T).GetProperties()) { - if (destProps.Any(x => x.Name == sourceProp.Name)) + // We should be able to write to the property + // for both the source and destination type + // This is only false when the derived type hides the base member + // (which we shouldn't copy anyway) + if (!sourceProp.CanRead || !sourceProp.CanWrite) { - var p = destProps.First(x => x.Name == sourceProp.Name); - p.SetValue(dest, sourceProp.GetValue(source, null), null); + continue; } - } + var v = sourceProp.GetValue(source); + if (v == null) + { + continue; + } + var p = destProps.Find(x => x.Name == sourceProp.Name); + if (p != null) + { + p.SetValue(dest, v); + } + } } /// @@ -93,7 +103,5 @@ namespace MediaBrowser.Controller.Entities source.DeepCopy(dest); return dest; } - - } } From 38fcd31917af1d904ea61de0f90918d44122d2e4 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 2 Apr 2019 18:19:19 -0400 Subject: [PATCH 0306/6208] Search all subdirectories for Plugins This was added in #801 which broke the previous plugin install behaviour. Previously plugins could be loaded from subdirectories but this search was only for the highest level. Change it to search all subdirectories instead to restore the previous behaviour. Also modifies the same option from #934, though I'm not 100% sure if this is needed here. --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 41ca2a1025..dbdf246a2d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1047,7 +1047,7 @@ namespace Emby.Server.Implementations private async void PluginInstalled(object sender, GenericEventArgs args) { string dir = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(args.Argument.targetFilename)); - var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly) + var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) .Select(x => Assembly.LoadFrom(x)) .SelectMany(x => x.ExportedTypes) .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) @@ -1346,7 +1346,7 @@ namespace Emby.Server.Implementations { if (Directory.Exists(ApplicationPaths.PluginsPath)) { - foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly)) + foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories)) { Logger.LogInformation("Loading assembly {Path}", file); yield return Assembly.LoadFrom(file); From d75324afc93129e9237607eda3d39248ad9719ec Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Wed, 3 Apr 2019 01:21:28 -0400 Subject: [PATCH 0307/6208] Update Dockerfiles * Use new dotnet image paths * Update jellyfin-web to 10.3.0-rc1 --- Dockerfile | 8 ++++---- Dockerfile.arm | 6 +++--- Dockerfile.arm64 | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5794bdde1e..dbbed535f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -ARG DOTNET_VERSION=2 +ARG DOTNET_VERSION=2.2 -FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder +FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 @@ -8,7 +8,7 @@ RUN bash -c "source deployment/common.build.sh && \ build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin" FROM jellyfin/ffmpeg as ffmpeg -FROM microsoft/dotnet:${DOTNET_VERSION}-runtime +FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION} # libfontconfig1 is required for Skia RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ @@ -21,7 +21,7 @@ RUN apt-get update \ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.2.2 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm b/Dockerfile.arm index 1497da0ef7..6bd5b576f1 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -8,7 +8,7 @@ FROM alpine as qemu_extract COPY --from=qemu /usr/bin qemu-arm-static.tar.gz RUN tar -xzvf qemu-arm-static.tar.gz -FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder +FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 @@ -21,7 +21,7 @@ RUN bash -c "source deployment/common.build.sh && \ build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" -FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 +FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7 COPY --from=qemu_extract qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -30,7 +30,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.2.2 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index f4658a055c..3be2fd4f14 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -9,7 +9,7 @@ COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz RUN tar -xzvf qemu-aarch64-static.tar.gz -FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder +FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 @@ -22,7 +22,7 @@ RUN bash -c "source deployment/common.build.sh && \ build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" -FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 +FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8 COPY --from=qemu_extract qemu-aarch64-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -31,7 +31,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.2.2 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web From 05a4161fd388d7ecda2f1b4a6ec6d02ee7b4488e Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 3 Apr 2019 19:43:02 -0400 Subject: [PATCH 0308/6208] Correct the installation and removal of plugins Upgrading plugins was broken for various reasons. There are four fixes and a minor one: 1. Use a directory name based only on the `Name` of the plugin, not the source filename, which contains the version. Avoids strange duplication of the plugin. 2. Use the new directory name for the deletes if it's present, so that installation and removal happen at that directory level and we don't leave empty folders laying around. Ensures we properly remove additional resources in plugins too, not just the main `.dll` file. 3. Ignore the incoming `target` when installing, and always set it ourself to the proper directory, which would matter when reinstalling. 4. Deletes an existing target directory before installing if it exists. Note that not calling any of the plugin removal code is intentional; I suspect that would delete configurations unexpectedly when upgrading which would be annoying. This way, it just replaces the files and then reloads. 5. (Minor) Added some actual debug messages around the plugin download section so failures can be more accurately seen. --- .../ApplicationHost.cs | 2 +- .../Updates/InstallationManager.cs | 41 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dbdf246a2d..05f8a8a5e7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1046,7 +1046,7 @@ namespace Emby.Server.Implementations private async void PluginInstalled(object sender, GenericEventArgs args) { - string dir = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(args.Argument.targetFilename)); + string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name); var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) .Select(x => Assembly.LoadFrom(x)) .SelectMany(x => x.ExportedTypes) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 7310de55d4..e5ba813a6c 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -85,9 +85,11 @@ namespace Emby.Server.Implementations.Updates private void OnPluginInstalled(PackageVersionInfo package) { _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + _logger.LogDebug("{String}", package.name); PluginInstalled?.Invoke(this, new GenericEventArgs { Argument = package }); + _logger.LogDebug("{String}", package.name); _applicationHost.NotifyPendingRestart(); } @@ -518,12 +520,12 @@ namespace Emby.Server.Implementations.Updates return; } - if (target == null) - { - target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename)); - } + // Always override the passed-in target (which is a file) and figure it out again + target = Path.Combine(_appPaths.PluginsPath, package.name); + _logger.LogDebug("Installing plugin to {Filename}.", target); // Download to temporary file so that, if interrupted, it won't destroy the existing installation + _logger.LogDebug("Downloading ZIP."); var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions { Url = package.sourceUrl, @@ -536,9 +538,17 @@ namespace Emby.Server.Implementations.Updates // TODO: Validate with a checksum, *properly* + // Check if the target directory already exists, and remove it if so + if (Directory.Exists(target)) + { + _logger.LogDebug("Deleting existing plugin at {Filename}.", target); + Directory.Delete(target, true); + } + // Success - move it to the real target try { + _logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target); using (var stream = File.OpenRead(tempFile)) { _zipClient.ExtractAllFromZip(stream, target, true); @@ -552,6 +562,7 @@ namespace Emby.Server.Implementations.Updates try { + _logger.LogDebug("Deleting temporary file {Filename}.", tempFile); _fileSystem.DeleteFile(tempFile); } catch (IOException ex) @@ -574,7 +585,18 @@ namespace Emby.Server.Implementations.Updates _applicationHost.RemovePlugin(plugin); var path = plugin.AssemblyFilePath; - _logger.LogInformation("Deleting plugin file {0}", path); + bool is_path_directory = false; + // Check if we have a plugin directory we should remove too + if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath) + { + path = Path.GetDirectoryName(plugin.AssemblyFilePath); + is_path_directory = true; + _logger.LogInformation("Deleting plugin directory {0}", path); + } + else + { + _logger.LogInformation("Deleting plugin file {0}", path); + } // Make this case-insensitive to account for possible incorrect assembly naming var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path)) @@ -585,7 +607,14 @@ namespace Emby.Server.Implementations.Updates path = file; } - _fileSystem.DeleteFile(path); + if (is_path_directory) + { + Directory.Delete(path, true); + } + else + { + _fileSystem.DeleteFile(path); + } var list = _config.Configuration.UninstalledPlugins.ToList(); var filename = Path.GetFileName(path); From 608fd873de6118198c2ac882b2859da3d0caff72 Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Wed, 3 Apr 2019 22:58:52 -0400 Subject: [PATCH 0309/6208] Optimize images with image_optim --- Emby.Dlna/Images/logo120.png | Bin 7522 -> 6201 bytes Emby.Dlna/Images/logo240.png | Bin 18287 -> 13339 bytes Emby.Dlna/Images/logo48.png | Bin 2554 -> 2263 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Emby.Dlna/Images/logo120.png b/Emby.Dlna/Images/logo120.png index 321c4772920b3b5e81c61a63f06d5bb2100514f2..14f6c8d5f66010de7b3f132c554e3ab53d49001e 100644 GIT binary patch literal 6201 zcmV-97{=#`P) zI9@)JOdMm9aXuUt0R`fQCK3|Os0oP+vU%_A?)UkSFJ%{=d;0A@2k?UZRCQUm>MiS5 z{doI4&wc;TdyG9?3r<>){&Ux=GbQ_;GmaJ@W0gbqy?sFTragJ>?(4UNJygM-x@+g1zht_^)4-v^K86aQ0_4MGS~zR?(y4W4 z)uk$ccLUXZ@0s>*cd7cNq03|h0x%GT4}dTPU<9D6*0ta7iMvKD+Pea{Il~>gpi^Vk zJ?-Gsk3pKT9b^MxV4nakzLd?Um)!RKXa1!ExEbn^p-T_Ux~fGOJ+}iH+PNSj1RV?+ zha+1wW5jdaDuA1j9zJ(T7w7T|1qX+BE9jFT0nrp>!076f`|oL^m+e;p+zd2t@RAOe z)~|v6KL*M6APGUH03A?x?SVm#|9j)uWgRPkKSlj)=z_hfO-~69l@$jG(6pO2Fi>bM z-P2iqIc?bDHWk31fHtH&+>Mg{N=dG0Ri=4AI0>5wVe zAqzuIfwTya5zqm!;QmRMt?E(%obX&XXL?AccF{Y~@1_CI*LoPF|5>iR^Dg(`S@F-2 zD}evLU)4#-&2_xWuhFtxu3ShsOf8OFjz&D%Pf#r?iguppqjf^$IT!cd| z+f5ftSa9x`%bz==0@yQCCDR=;ND_;}#>yTs%O7#+J^c>3{O*2-$Z?p#L-K%(FjkR| z00O&v{P<_u(~Oj+IVqREThP9+V`9dl0=Ny5&L2Og-_I$CD`)pz0}Q@t)YAEq%@fux z3^F2WK?pj9wViWSHMavpDm(ZSdi1u3wqE`u`yaG*H;vkszCdZczsw zot?6`Rse7NYF>Oc=u83LSmbxp@8z5g48!$n@*REGmiRp5`)|X>h1|4vU;qfO-M_xE{s*5)!2dpI$+5tN z;hho&>37ri<1usk&teB@%I(MAY1loYh?@=%&qu8tKWy@sNlHrp7?7Kd}Nc@8{6Ytv551leZ>Fg2Y=fsXcq^tR@|ayt@JRkKjlX0F*2iJ)frPWFiBFTi?NfijUzmI%7zpNR<4mI%fpmlUdde zk|KvA*4~kwv_}B0J9o)Z5<~GH9QdjuQ%29}{}PFn{Hq$XAC?HR>0NFhWAryO&mXee z114}YfIuV}!?73AR8OUu=$)FH8)dv42I+Uxg>_UZu$y;lz>l7{@J!&O0<2i=1m^-b z!t>RZ2E_a2Dcl>9^KvN|*3}L-K{QY6Wni*jKjFl8jY%&%BrMhVXyKJj# zeuo75s2q0f*yRrh=9tk<&`qZMCS1DwzDz~|2{iAAecm-R$>XNI&u2_izBqM7yWIw` zvX%k06A-tZV6bq+oWApt#To@YI(K=%VM9b{_YC`R!^*^v52!k3F0j<=rY*uv1J3AB zm8-gPw*dU_=PxzpPSQU|UN>ghU?;BdwG$jRY5e8SeU}89cf(P? zmH2Vgtv-G?4KizmGe0kR9Rd%4M3IUm1&lM>xL{o9_%ZL+6e)F%-4J{izIrl9&T%3QRfrNbT; zR0$(yJE2PE9@ts|Z9z=cUE6EE2$q)Pro+!?`}!Add9mLv0_@M4U{i{<6Xb6v;EbzD zqT1Z#@V(KYkGGxRup>UKsfnLS&1C3GubU>)O(Q@GrZo$|N32=$UErv=V>kjZ{bJ<3 z{hvwVn!&~k5Ej+nE#thFt1i6eg|6`rBaT{Z=*h6LA`CL}chLzaOn>=Hi2*0yy_%S? zo#1kTOH}X32l*cfYNBG1*G-4p2`z$rZaPV%^aQ|_Rv>Ts1j@->ml$xu-Fp^Bk0k8` z2M>;!eZmXGRA%JzuOd$Ol(xN7o8hl@zV*EcD^DY)9?SH6QO0~vEb?@&tWV!tUispu z0$9i}O-$HMXbfEb`uLf5bu}q&ExWW$iS2~IZoBBB6;;Gk9!(ZxJ+UatO@kSrYd#gg z^9L>d25@A^NR-!3m~+YfC;X9^3f|vzx!{nfBgoipn!>B^vK?#`G4-F5k6t6=0Rd1H zWb{8k(-)4N{>q8F95BYja>j5J-c=1jimAlZO(U28lM@pPAi+1asW|Nf*|p~;xQ@Zn z=^Ip-r;Y*{?WPT|_`5iWn|lSl6O7@2bpMEZ`oBd?HKw>y&_)K7(cg{HPH2}-XU7s# zk5B0FXVAY1znhN!y>)8u87~jmWq`r1PBMlgBWvm4W@0LH*|rkbC2?XA2IaRCYelY6cO z>PmCd7G)4mKl-+pdhd!Id`)-^r>u5DUZi60-W$iQ=q$K6X)G$cod9D8XI|()OyyHH z1#l&f)S@tS2vS$?RKWAMzS2hML;zW=b^@3^GV0zwuf^ZI`H4VF6^TWP6N~)qgw|Q| zv3K!PSM_{V@R;9CN0-eV`sU|myma_Z11trXCB7t7?p9)I+sh)2;SpKe4T+IRA6EztnMu1GZdxoQzbGcPB`0Mv{wLFkM&_ zq?H_t{01>%E$EU&lbFVgaz<*yYpq5>`r;i5_@P0|+M01e0VJrrScHtb5>xZfK09wPGa97dP0Q4wX495)Vge4o z+QylUirY?*fyBFwEqRcbx@O$c!vzD&7{l?@V`94L0`#w(c|-gu%)F}Z$M~QC(&whb znHg+6k|hSbZvWJ`fdk^S6AF|2dLYT28fgU=2(x4d2jR7$s8xD9!LrT<6H`mBI&7nZ zhs$%*p`Uwpzw^~ECI$>PE>SE30(3tKn9H(9J$l-x~6w-XLM{f3tgC#IHP(ep*b z3J8Jp7sw0E#M8RXdi9XlfDMNG+6nT_UYX{SKu)QF#o7t-gDl%km)cG+baJv$Bb)2r zI1!oM05Ao*Fc#ptvlg^=aJHwN;2Ub+7^E8KlR$$0F&%O!&c%pDo_2zcCxKel@~~i| zCl-0mj_rbj?^sh)Hel7J&M~Y_l6NQMWpyL(8n`JT;O?GSAf~xiZ#SYSp8U5b_ zCW0nie)-DdFnvL?z8J>{_7?Z8x<(*jaF( z1?rTS{#n?*7}ZE^M39!L9T2Es*w%w(Wo!bg7y3}fW*P4yy=A1n=TZ#teZv;FQI1nWAj@>q4i>YE^pniMdf?@4pWR!F zWOlqi78TiKvafw~jQIKmUF(}PSuJvQhM>0ec0yF1)Dp^4Mb2zVWJC-7H)7b)XtODw zJGF?((znWNC!jENS&N1;%|}J)e8UofBRUp^Pqo5P3X?lC47l*8$L&ZU^|6qkmZ=^ko6^jNy1E8UqEyQbkT? z0%U%W(PcKk^bP8i`WW#0AGGZcwDynT_}w(33$av@0_`7m$i>_=QEqylb{l_x^v8f@ zcCx>nP_mmoWZH=54y7zrblBam9ta!}cF6JEv3B7fBqXPmUzwg1i3u`3Rss4Dv0oiFX5 z+sA$_?9>R59ycACnfppV^w^f# z+!Ht7eEerJN_~Fz+I9_%^#rI2>~DlEaJc20^6gj6oY}o$&jh-nWp;Cm&j*3^&7*-0^$n-Uthz*y z5e_-}3_QG6z0%I;+BC=#-E>(+FGEl(__|DA7e>pf^^I4Jf9|(=({h=uR?607$*q~W z_2ta$Ci>{LIB&dh=mC}ry64Th3+o~1li$LT7Lax(KgfttU4KQ_qVM<+-O>uIn^Y8} z-%VRYW-hHCR4t=i3E=Z!U(t(EC5CtD}uZJGNUF7;M}%dMl;=Jnf- z3wNRhO#5A%nrwaBd@B9V7@Y`Q=e${GL1#&w6$A&r@xL5IK#c=X{Xpddhi0WNks z0c3Gvk>5=_S;w?QtrEo|4@h5sH+eguN=AD_?PZ{XVZ;dv#zcjkBXFkedq@JzpviWh zc|1s>c7o@0&IdA@4FN3zLCV+ll{Xgoi(bvJo#4MDv;f7sHby%kkwX@gvbH*qv96?{ zc26wwx#^wRPEgEtLV`;|L+}%{6Z{n*^SJ5A3+Y&w#&}(@zcBxUz6eNR9yh)7+6g3T zCwM@{yA20LTL_I*>Pkpjsd zi;}zP2S;a01#de&VHlLfuf5-sHOh_Xca?qaxfbY*8@Q}oJfci*V9^sI8 zSS$i|LOa3#Rhrx(d)E45Q9?HzhTZ@+I6tKOgWGkJF1fG&d{95kmKET{aMQc?lF)c= z+TytB5FC%2jvb3UZaNye(BdEnj?E7~=;0=t4If=In8|3sUVmA7p2M!fI3k9d-tq5F zD90iDV^MN9Z3*18KNk7jba-<-4>m0~_~1t|Q_*fxCSwkmC-mTqi0T&7T5=K?X zSa~1xiQKdwB;`EIJ#IQlEGp;k%{%OnfqEH_OU!K?+Wj{G3?Za65i(57=5gThn?^6( zHxXt- zk4I_zV&cpb{|?LoW=-2#P|LPV|0{t`Hra{J=pir}JMEHC|5^{om~PrX(dc*63CD19 z1}lXv#_Xw0q@HG2*Lr|0l%_Og>Z<#X-v~Um?emYfpVSA|pMYb*jsYDbqw~(XBvcqm z5{vM}q9m7uCO>3YD_99yl@EBXHeJ7b-H=|JNuV?dHSx|q?*fni;{(7IS3KJ(B|8kK z?lS8x=q}?>K{w25cETm0o;VjP7A14j(I7t%tRb*9$i3#^wZPeH4VGT>{zV6UKtX&! za;4b`y5*MR{tEmRc$$Lv=ndN)yL34ql}aDtq^<<&gn~~0*zp~O{spXqpt>wKO?hrw zo>-LNlF&^;Z-V`efVaW^irilif3*~^TPpwiKMn7>j$NoUyHNAzpPdK(wCz(w7iCuL z(~wVfRGtn2+DC!f0Ig-T0kx6Y-h$Sc+K8Zyj5a_u(8>Xrfl({bq5y|hf@&E64T3EN z6J#FN%d}Ah8zg<;Wb0biZh33v#l5%C%=G^N XU2ajqLA}4W00000NkvXXu0mjfyde4m literal 7522 zcmV-o9i8HdP)8m+ zjLQ&N9Z)2!f-6BZtq6h}Dn?{c6t^&Z#4IXVOh#dbMFBUKC^0Tk6O0NBi!;-G>wG_Y zyGzyW+xK?Q0Dk9r=3y@POxLONo;v5Px>dIchq+XpT^0VgOXMCx6nvCqad2L?Z$EiC|hY zZ`|w0HhvEZBZmdR&kU{UB&D&Mf#dAJ+5ZJ74Y(6z0E8}*xArJ7f z5jE{7%zpxKbQ(wiW*`AO$fj!~Q?^yNvxwtwv#wp=zVUie7&)W?e!imWh$7;b6Vsr? z|LK4POh8)hkz>RsbkQvw!%K5VSC=+ghYBEvEWnlJm7zKrd0GNJ0ehXz0;EOPnW{~? zy=6&wd1ZNJsPTH1UpZs}mL(nTW#B?ptyKc4AXawJ0o>rcpnBul+H1HeS#2D~IGX zY5BR0HWbD=J$D~sru|!4mp6I8~$p4koX%D6=ww} zeNVYesRYuw+LhL=li)$o`F+P6F+ik_#9rAU&~ZHIN!MZPx7|1w9|Fs46K~NAoO)48X-` z{tyGb)C}2l4d2rucM_HkCMt`!YH@GDJDevuBx>-A5o^i~pi=wbz$39zK4j36u*_$w z>bwMBa@PZA{kO58t*ujJy43g~7%?_kp*K&`<(8)KvesO-i z5nK)`fGaDiIxz7=8ON%COx}Z+RGI>fo+C0&y6(|_$KSA^-|;Nboq(rxY2BSoljYiC zi=YKlr>voJsiXQ+lGVi|eDF+RUUOs%y5CWdfogr&=KLgQulNa{g=GN}) ziEFzX2>pc#NBLsYsrF5}kaW?n?m7L1f;lS(IVb?G9JTt}h|v}X+F5{9ITD(wjzyPt zg6N!V;HD%xe(iB2La(sEcRfst^h=s)UHUf=>9JtWDNVk|`rxR+F|hz?aLo)>L4vZg z>72j?faoLr{~-hqV$f%v*mSBr`%|rq-5n*z?#sys1(GHo!OMqN4<_*POnOv|2slTT zT{n5}tGJ}IrY`BX-H$ENi5t5cAz72=0j}l3zBaAMGDH_Fp51#@U?&BTCLO`g3|(_1 zf#>TOa@J{)RWHs=Kl2MN07M@y`#UiPGa;^=Gs7HUdN!T8=7r;-<*9rmg8xGF>rgNx$WVwD|4!X6<00Ao zxoFuJg|yc$jXr`Y9)1uo?a;6R>5a?)67)!+!3BW(?(6d#CM-1DW46V?rftN~^mpG1 zacEYd?X^pz0$e$~`aA{(>wrxEo}Eo^jkV+(8q4lhE%!}fqPLxPt4}bhnLa@>t{!sh z=6;Q3ce3fmk);(?B`xCNw*hpsGh{3C%z8SZUPvw*_t=0fjb%FkOuBhD0ANR}$XbNxRQ@rIFt5(`X~Or4gA5pqpv|Av-HIKdYsgjSVgU%%9u)EeV>Pu{#T! zwh%-MF+8;$9g%m|uaiar_}L-r+cWbtKwrB&PK(|QBXW0R*sPMa&1+|Yu*qUu@?Hu^ z%RK^M8qsMz#vSF+t&Q^BxKRL(gk!S-w9-s<90W@27>QU7-eZErT2KezW=AiB#V zkm^yMsLhiuiN1M!q3tdF2o`c4kk}mw=zvV#gPl#M>+V}&6wN8Ptsdoz;^J9M*rEl} zl})GhA}pxj<))wjFCV`8G5}|5fwV1jXtv=W&7Dj+Q+4CZ@e z>my(MKexYeULkEQ7{Ha~TZ;&E7XUqw$(wF$S_&+gxNyL_LfPq87R@cImV_tNY?^HK z*;m+>1zB61M}ZIxw4N5ay5B{0zI>vy}42Cz7&Jt`^4LPuhcmDqu7>&?pjR7fuh zKv2c_Mg5)(VoP~>W$2{ubb4HZMq1a3`SFrgea1wiB!hhZ=vy{jV-PJgr(tU!Gyk7h zV|Vr+z~^=0*zqq$0+G2M5X2V@YTiiAQb+Smin`BBDh>mm_bVyncja+?efjf&|?%^@i842-4QFnWw(N zjHM1Xtp-xTrU52u0gC5+@guJQR}Oo%Cm2^aqu9=nU8hC61x0=t#EzMhHg=VuM@%OU zoc3fuCjd((-}pwiAZ=1OG@XIkbQ`&{X#kRN%{h0!(=~`4cFFGuCYl4U+i(C=M4KES z_|CD5{67?rMytXG8P71Gr80BaKu`X(AeK)AIj#RxwAbeh%&`Lr*!!qu(*O`5gQI?J zP)XhZt{ho?7C;v`fOMos>ot(+APfA{DBct780J^aXE1PPPG^0Iz{LGKb~!c>(b>JL z<^4=}U%xi(Je8=Z&qD{z+0rYB9cIbj2$p;&pa2pIAT?|n!1a^oUr_JYggYj$KOMky zEs$AGCrIG-i8pO7^V|Na`Q7RnG~IQ^pkmXo+!G*qhhH1)l9zdK!1ViW(Zq=FRK>wJ$@M>GIp(u3_SS>Ca8I zZ{)E#fdlW|@*}@Cq$Ci) ziP6i`)zJyB+i+xD$Uw(C3}HOTK(X7j$QzqZ2WK;3#Dl%Aki*jeNTqLd_1X3WO0o>J z2m=HJ_@o^*u+ZRIS6|*tLG`{KK>!9|Qbsx{P>X(+*qsys~*)2KZ^;=Ln`>1lfkegcyZ| z?+s_s>o>LqFg{IKa+nrrYoKMM+xJt^)FAtqS%&UpqPU_2*zC6X)U3oxfRpEo z26!Sf8qfhO8~13x5B(Sn0EijW8EENuT9ncAd^(|3k!-lmueqyk?fE`{N^fkMbUwZC zwEN%5c^TEu2%h1nT~Gk2a~8p1gnWOnd~?t346Mw2t_L=416_hIIeFeYC+5;_P5?8D>)pvr zmeUCUz|!~oqh?*ZzC8$I^r_SXn^p#(dSqMJGys4m24U0&FxQ+>+ygLWi$#}6h$*?W z=^5bXDylj&;bL>6xopEp2JFid77utc7X!@_lVS`5#a@6k+hW&M*E#h@1WO+oEn+2d zlwbR+esRhs231?_PT6L)Nnb8-<;f4cPE5kQ>;b4BI1DOg5%zri)RUo44_n`g2&3%j z#gw{iq(I?t!n=J zo7SzMXMV_`O9F#~^vOW-)l9jtY1nl!HCsEb06#Zk&6y0G8RWYYKv+9*@mX8FbXq0R zDeHPO=vZeY2bdPYnewzGOqtdJ2Ay!>%-j|o%(CIeo^OM((Q3CQIF-RlV9@!;KD^!a zEdj0oQ|gS7>Br9e9JR}?OS~lK-kU^nXpk~>+1kzuWG-wPwmxY}0D;d5(lGo@SC&_`l)&I@rxPsq?6nKnD?$w`ym#3t4D|QQknO3?P1g-bXCG6# zd{&UYH7~(44D3|~r^XhA-J_0sXh*TG4K2W25*k9FQf0bw0tpqHHkc|WJThos&OJBV zk$ksS^7kB5sfL7ks^y7BoTJJ#$^`9Uyk^-rc&&Pp8eog!giq7U@WsjUik9PFbI`vuP0C58A)l zNXcJ-%$!&Sq@{hcJ((MbV0l_Xy*Z7)P)*;@FNZiGYPjsnN=>P@jfL_<~nKoThJ^oOF z?udnA$E}*tYkfZYXb4)Vw>$N_E1QNS8jVVJ9DjAZWv4_&E|owV6_7ABU}+TQdjuvN z5Ru(~3EcCf%$wK$D*)30zU77+HuVAV_nE0L2)!9Nnv;Q=ivH{YsMocz={Ue2F=!Km z*0aRw7jHXdQ@;9@lIBL~ANPyeItEI#k213cAdc#=YsdNj0bVqJwgX@Wh5{NOJ+Wzk zf!D4La12y0#P{Bd4FEQz{-+{ZRnlr-%i|=8&Lp^l!TcQteH(-#Fqbk!n2B2>@#P;W z!nPZr7!&pZRL9`2nXsP;UxN8lLTG19@IQ-<@CPfRc_=ekWZiW~e|^e>TQ@RrmLod> znnC6SorD|;H-H}-U0up?oT~*AW+3%!S_0L1>A44aB08uJz?S6Cp)6~d=qx3WNd+|r zFtNCOWg{+oz!R^GB7o(&kOnZCP~<+ui?aba3`CFvmPsX1^{AtnfY@m z^-E=WRm+BEkSvd^acmljAJ`yT5D| zZ;!zV?vNhA@#c{WKxn37$gXVKHiZK4QK21ixC((k^g1n4%*#{vj%ZUGN|i!VaX1 zPaUA^bcyI>dSX<~I50or{gwObM$J)W2VDYRGvP&mR~{&dzj*iEXMH25Ql~F`zjb{> z-9?h=GA5kQK#8kQm9KE| z`+ib5*XLKtD=R}EeRRSx6cXJGqHYZ8!N8d^@tUcyvj0$j>9w*1B8yKQy6J5EJ6bxr z=3VB*yQZ=N>3MD1q~}t*^|}NG_8Wv9AbOvPKb1^7neZ8t{2Vg$X}yf^M2r2qlaC7v zCIjaFy|g%1*UAWmzXL)C2-(pf>c~JxX70#B9A^-^0w`AYN$1lXbsZ&8=TnO=zfDU= z*L)=zNBRcRjRe@E%B^{YIKbd~1|UOd z22hF2w0R^8Kr%OH60Ib-6hy5UIMUL-TX5RJaqE-aPi|bs6Od{)Eg3t*%qX$efLtCj znDo-WI~T-#y$y%Vx~``aB1~vQ;5JMEbH+BwW~Xz;ZU!WA`g0kHze`R-75kB$58btV zg%o|v_TWI}8{2?!_}^TkHP}F)nP4EK1dhxNdwIwJH7V0M;Gj93V76Ngq#~%4y8cfm zD14f>n=G~{vC``n2`22cj}&{OZqaE6Jqw$5cYq2=52q7!LFE84tw#=BO?GgM6cv!_ z0Ceh_{p1|w`aY@|vONIFp$lNA0O3OiI0|F6Yty!fc6C-d;#8n(fK)k7x2atffSyyS zJCF|G9G|ZTGV2H=Tl?uY^y|$^-`I2|(Z>SdReA@IJtAz;sWCjQYfTxRfz;^2<}}OS zDTB)9V5oyLw^XVI-ST&?Bajrj9)Yw6vfWP(d-#unDf|?JYT>x4a{#G?K z_Kvv$>2QV%o7P6Bi$hYM=>SsENtHeJ;IxBd{yRk0RTT=GT>=u{0u;ygU#6+uzXM!AnnGg9N>pdle=zKHmw2DFGKdGn|)fO z^l4hp1Dj4<*8?=d7}@P9z_cf8{A2xB0Nf8m>vceSXVVG?6zV0RnpEees|3FIUC@l_v zQ^H`HH+KC`q`)-{`VkW%*&iG@!LaMPeIvL-H)kN7VAypn?W-J_Paxg;q}0vnAwwM# z{=keS^%dRztiw?YuvunLTK(;iQT#IoUn~jVV?t37AbqSi=d9}nr0a84mp(aw^b(v3 zAOoy79{}TJDd_h_9A3G5RF~b^u=514S>}$eE{!!0{}2M_Fw^%8LJtN=AAuT2W&&>L{{f*VwLm@{L770Mj0 s%glQuXlL`11D|dhTXvuc-KLcP1N76;{1a)bOaK4?07*qoM6N<$g1~16F#rGn diff --git a/Emby.Dlna/Images/logo240.png b/Emby.Dlna/Images/logo240.png index 64c8281299ec62baed2ba1722ae637783272885a..ff50314d44a8d2b9ae1a23690a95c3983293f8a3 100644 GIT binary patch literal 13339 zcmXY2c|4Tg+a7zNQ1+1}vV>$`BhuJXmKd@X3C(EiMjBfYY3veN5+XDj`!a@P-^soW zvWFQ<24i_2-{0%=!5_{u=Q-!x_jO;_bsteC_w_C?@-l)zpbG~2x@N#{(%JVpdf=y~ z#cBcwBtLJUtNjo;v7SNaG1FlYnh0pEan~MQ%D2Ov4>`>jF_C|^TY07AiHv~|L5Ky9c9wUKWhE2 zct%xz4$}2O;1&bJ#L2NPC*Qd+?u`SoH%kQvln*XB^YX{{W|s1l{gc%7*CCCsWS++A zyR34y4}O1gtomLs(rLWf+`r*@SHybwxpxuVm#&t^iv$ca}H)68`s>%lJ1)d4Bj(SpV zd}2eo>2O_I=FwIFJI&3(ouS&E zv8Y1X&}f1?CX}Bv$8jYL9n9wj1qywuPi51+Y+HE(k0W1&6FzrCz|(yYrH$xqUhAZx zKD%-AKziZGKL_+3QE}TYN&ek$8KA6C7R*9fyfNhq?y5NP9%I=q;b!e>f=WJ_c{*nyiv(FWNakacF8;s8+D<$_QVgZ4Y#95|Lr#`wa8KWuZ;%VU)e75L{fq z)o*LhI3J#ycgc1X>kJer8~6$8gGi?LL2!?M1AB%fbswJYS)75kh3egAbQnWdLkZs4@(zepi8_q4&xoQVDyzSz;R{5{HTM|%e5TsCkCIEA23dv`-!v=eG3N_e!Z7hR+iX>{zp zDfD}P8QC^mZ2la2e|)1j{yVeuI;P~!9!h_Ews5P=bOeJkeZ2pvD1uv?PeO~*vG7TI zDsTaauTc)E?Y#o1`--Jj7vF1iaWMR@ZGAU|oFR0B-!`vQ(VU{=710=FyRoEyIFxcT zOr=TS`a>a&_A)kwlFDnQuVlSD()3pkx|}9*DKM`EJr}W!PlW`$_|4eG2ZxvKSRbe? zEoaZecLr1=M;G9m!xyiRW5) zgqG4%NvC@&g7fTpAqhF&8gI9Gqb_Fds*Aq<%%#-50nROvY&@;!8F~DndoWRul+*?D z0wQ1}r_5sJ$Ux^YKaa+N`g_5(qgL|b{7XJ|mB*tCjSlXbJ5enL(!;aljKz&lT-pIZ zya|u`eC_Ic_Us&W?y*DQ@)gVByU*658o+i}ZMB|BOA7!?I-1}o8##}3-L#Oiu8Iv-`~<(1(cA>X_=V5+yvse; z!VlfUjkP2r;p|1LconrWl1^pIW^2K<#p5W&5uAb_ArC~pEP;)04RqnlAe2s$kIk0y z?9FTshQ-cHA*W1`MgH$n$jM zTaV#zS`TZo^Q=2Pw-?K`5g$in{`y~f*9HY^xQ~=_;r^4spQ2PLk3z@{S8iYgL!%`z z6naI~0dH}fxy!GS=pv{D?yGg8ljP}%IOqUO4fEQKM1yWJ$>`2qSlizh8MSLp>wT!txv%*7dpP^$ z?;QvJOS>_r7RIk|WF5Y2mK8Q|l!I-Bhl8u*rKEM~a!ZoWAcv}`8&u^3m-Q>HRS|ha z=1vgk)jb8JK}|JSoeYx<9gvqL%5>i5?2qAbzQn5*oqhjGkpIPJJ0XZ(F7?`3ta zzdYc(jMXwUGBiKPIV9Rqy5_W)TPq4L6PsWOe&J!Gq}>wvrN7=w$qi2T;LWOPcbIXV z`%g>cZj662P}{ajMVJONVhlzWUZtwn?>`ze5PSAy)xv|&cOeSBujg`(q!R)i-Wo8- z?Cn27*x4GyqD64tA7nk5I`!V!+eS4*s5gt=TR7Xf`gRBS>ioQ9-U3l-dog&Ies*)< zw7%>k#Vn*v>UF{`%Ueco8>+)ZFdYZCpFb55dPLp$wuPWxbvplU8P7gD;78OGUY}u# zQoP~>+V~si_`xrHDsp}6>EedV-;)UI?tvh_PZqaOV9 zF}&HIAlPVFJxyV8M5hwexgcdELx;r6bI1=qTdBz=ni>-YW0AGMw(8D>zv|Sl^FJp` zu-cigZi)tN-W#de=Wx8>4D;F%zkRui4WH5|N zA4Qeki3gNO|Me_34dSdBD~k>E%=3pgtE(x~Xne8$_4`#HE9_|tgf1&J*KST+jv)+eZPkgB5&%mgTXA@>* zd7&sY?UetD!S`z;+#q7^{97vb9Ep*I$0hQz0|knoIc%R=>dV{u(Db%dgaOow(2OW%eAJ6fN@8;4wx5yQstND71<6cTPq)XFWYef zsN@SHsEkyaHx;-su=GtJ6+h%H4br?C62~>6m^Oipt|< zxSUFTlQz3#A4^LWe&^mLu;PnIk6k%~o@MJ0vK@!LG}+mxCiPK&aBK5NVH&z052$A# z!j3uF8f0SxJvZ-1OW6dgoaMxhDw)r*O1UK+Sk=nZMa{?7@bTylY)FbPVSi*=nQC5P zjd)d*r;&0sT)7;}*0@-UFnKL^{dwT3RpkpR4eZTdBEZgdRB_nezjqVlzbeOA5<=PL z{kPof+pA31XoP}N&kAn@6Q!2$4?L&*;|e)AIzyNn&?~Yd@5a7Jf;RlOorHIMc%@^d zuUtevpq)+$hmN7#q*tP9n5MqAmx+ngZLANdKE<+ySLFfe8Y+2IsLA~U@|Hnuq{n;U zSzH3?xM%+JlKiXd=a5x}r0r=a zWu^P<6Z``(JTo3NGjb;b)+CrP75R$(G}I!3@n-p8Lri3>BI2IZt2oWnq$!L?6SMPF z*PqTN3EY1aptR!i^CIea4W~p67Mht>lQu|tjy(IDDhCt2Y;dGv(`H>CGnQ@S*Jj)% z&bt%wqb(Vg*VF}od>iByc`?Z=SQeJK{AiU`W(WWL_E{;(qOC@ls7ESZC2?vvCycGM zxAI(r8isQPh{W{fUB{(i2lzI|B8hmM*$Z;rb2sI96{q!?zpYX`$DelsU8u-(ILTwf ztmKodmnG%Tt!Xu_#J4{>8pnJWiPGm+Azo4shjOskZ;eI%!Ka&T(qUA>;+eM?d!i+O zj_Eg|BpMBg$;k-T4eZjtsPMUjNsg7kWwLW{0U~Ro_5$fd@TF(o6(Frr%sY?m)~Sg- z2?T>ICZyIm6r9$XG?hZVj#w|dP67?^6{Q!~`jVyPcV!iy(QnX|U)0SaP4rgFyVqo@SAZS*pbQ3%gEg9L=1v=Va zGtB=tpd}yThhf9UFSS_Y{&B8hQ8K(ikIOn|YkR&0;!QhMm29@j0zZ#oifj&0YN6-> z7}Y&+R_AJE=&Sn#>_PB{Vj0$|UWUe?LgzY+=K`!}f{NKuhub-19cvmEcyDta!A2$N)5HYit+ zuf>mkcC6{st?O4k&F0ew*zK>3ESzhFOl%s>xtlVnh3cozb)jkidPwV(V~D4el5}Ek zvy^;$)#~Mt3iG;ZgzwC-@QcFnvJ=BY$mUyP`uXHucObKKG<}d)Ihpm3q>sSWMCGZR zvR*7(A4zBT$-F*Cd!b-r1n7{U;RjoAG~@v!!!G?R>qxU_lEE#1`?)U zU1{ywEIy>ldRSPBoZOEAk&lso%fIYA4O(VG2D86MH_Kp_>fSi!@bGSS4TS}1N&NIh zHxrt;sAv2B#h^3ci4^)d)VY@?F-%?l1n6QbWQ}0$g~;p*LX3=w160lE^M|~tsLTxA zd1m7sX~w$%|E`!$p~hY8WM(FsZAy9?U;IvYYxfayc63{l!)MoVj>A6D={Ef7pK6)> zq&hZAxs&UCWdPB~xi`fhn?4apQK z-GQ@9vx1Eg(?HQEc(C;L^p55J1H;HCzZY$ zzJP*oaXr{LeO~pLjp~3CfqHw&kDdsvrT+TX!E}AD_S+Tt~w>H2? zq8F)@?Ekdfm$!t+=Iu=W!Jik3Mo%>XC5`L(d4x$v=Bn->b2-Be2z>Vf6QbQw_##d_ zgvK!TUuc70r*Wd3Qw~lvB>HmR&uFK*!>5BQW8#pkv6Ugc<4wMz2+-8xjCg z7YwJZc*E7nD1OVrdGw9%*C21D-k`$~*0yG>n^C{ghG=DmVHwV&!>lzhuP_iIQrsZ^V3Laz^cm%I(GIR)w0w4$ z_aw+vsSV+tp2^>!@ZBE(pGOAL`hL;7KfBla<3_&8^Ew^YS>bBBhKKW+MJnev7+Y50xKRx2P(*t|gZ@mV>GgU$?S*+m6{YRWPL!JjSu3)6 zHE}K&x;}5HaR$&dq>S;7XYh&5kfEOC>+yGOXt9LU{O74sA-?OaKnK1}CmIKqz;TsbB zF@gZ;Z)C!v4{f4~f9mbkTS7;;*}&7kd>zE|AJqtpnfw;A?8nRD#qDR(_*zl!7u{5! zuC15;r6N5Lb%(OXqML7EtjKv8c&+&WXJxkT<`|$RZ zpaVuto}@8g!%wW17$|#I%P-3+CC7BQz}nJt&Nlz*{jRz-{qSP|1&?f!d&4%n8o@pe zD|g!*ud`Y4sczn6Z@sIjs@;zWWWX^z(`wBJjmBCTQ>vK z^wad9!}e6jSO9hUZEdk6vyklRXU7|qxMoFFc6&D#2~|1&*cEcyU=#(4=3I@8`|yLU zX>E~f`b1#t>ktyz!%} zcLGqI{qe)F6SRK1eL$!;6>*e>`x00di07iUh-H{2bC>_IVN>l`Sa?L0q{ozUn`Qca zk^9UaMyO^4YWvgp7)#uIr1uqioe19G2x4A+|`tQ(VIr6@Q&-D5GFIl5rUoB;LGrDm56%rr=pH*VLQpA_Y~E3+HWY{3T{@iSmIL%f*z!9yKvh*;{1sl?*boDX zJy5<*;~y`md(;&DD(;Bq20$Fjle8%*D#{Hy;*9ByXoeJ1?DeL(YM(iM_v+cHxU6&X zSb)TzP%iq`*WucvrCcvNsE?}gN`>9ekTWatD?>O0#UT5$5hVt*w_W8ps|wVw{xMaA zXZHk&vHwdMz%+Uih}->DM)z(S{pmXCw{R5BCin7gs;d-?Mx9^!nI5r2MI;~M{g&B}g2N%J)HbM+pZ}{$lA9HL;Q&GQ zy`VIpx8Xt90zv~b>Om+9Q@dwx9>n%;rTr^_0)S{c+4JS=iITXL_0e_R`%EVwj9x%* z2E%nTxT^XdIPc3`73Hl6VdjFi>F|MNNjl6p_F2j7BIRM`pCH#vW)CJNhs4YFzmwSQoJuqD!QrlV#_@ycs9xA|ZQ#;A28nlvI?zo0?1!|oAcGSo zFps+fm!0mIANS_}mK^y*Ks^oqAt>jMD5%FzpRK|1tU|tC`{1B#q^w7UdwT%^ovkJw zitB~=%vx9fSi$WdY-$1qtqfIl!|OsSqN#k9q-!;it(^wx!1H10le$z3#TEIi!#bCS zCB16X$0-L)=P1kxWm{S$snQGg^0`2TKEnaPEsGQK51o8XfcPuf_qK}Tlt zJd7m?pQxt@s)qQ15vS7Y}%IICL1H zG%78eFTcWl$T3BhcbepQ)Cl;#S(#bZ+=DAWd0{VpEwX_$+1^D<2Em8sSEiVW2<72# zg{t){u3lT_fLNrrx8Qedv~s#IHJX1K=)wl@wSL6sE~Q~YpjuU5;6ians1-81xFF%x z*{B{n)!|PQ^LXXxHptb@Xki7=j9EiMxyw)(ypd9Y-C%r0?lDvR{W+HW9SiEkV8hjv zYR$S(%I3rT&!Isl^UX~jG?N?(Q(UF0FE7!w^1}La34dbtFdb~Q)q@^{XLh)Xxw*Y5 z&4w4cbviF>ru-xRe7vPWf}PEcu3h3Fos8L~jOhdZkGBIODX;&U4XiAjsS89@ZmpE` zI1^u4+$tkhG$DV*agY{>2D#pc2{dEtZ?(exeMZyBD)G*o`zpThM4OopR$L>Uza;JC zBpd@6huKV33cIZP$2RE**E;I(lt1`XdBMN*6SII1yR-^aLlK}FBFb*e*t#Hg2K}mu z!MpF#6DZHTCZ_M7=Byy^ufU3hrcxaYXMzwCZLN@yuqbXRUn)9VLH&|0(rW#Rt7XIu#t80L^4v8d-3yDJgyS^_Rha+jmWwbidsR$Ub+V zau)KAWKz#Ikyi(-z2LN_NFxiyXWbsQdd$4AVni4f(KDz{2hd;i_JD^17@ho@xZm$y zB=Z7_E)P^yQVbd&z!r7O^4-B@bP^*1t z&0%-gv-WWwY;dj(2=UHu)r_Q|ufCqnfEDNI0K(PVtNI7tQ6Z9v;Orox)I1(maCXGW z19`(mj)o;yg}8yk7nbSY_%Owr^@iMwJM-1pXAimbyQpviyn`6QS%BcNKfdNr7^~Yvb(-ps5m8Qgdxg;|o0-uT6JOU0p2M*6lW2#Lu%O)PGS5#QgoE#mlEH>w^b= z6ud*iZ(yF)@Y9PweV3_c5X?4-ztbq0Yb)KLSG{|Oms4JRzZFYWjjSF3IvOP(D45($ zBdUl&mp?>!zSgbEa%HzRAmKJ@Lh)6L51}42>RqwuN^M)@Yc+4^uf*=H8WoGywDfyt zBpB#i^xS}%J%9f@4~EM>JvZ{AJCN)ew4dL_lV!zvPz@U2Yaj6_ren(KR3I-;kNcJD0cmDa|$k) zcMn<)l$1Q84Cd^7W zSR^_5Nt-t_=MV?Y6a-MLYApaeP@=)*JkiSLvZ61SJl6s=&$=3!zL9jg(vtb0UqxC} z!I%G}Z#E-c49(L2YX6-t+UfP3WXTM8;AcTpA7!x2u@e$(0EAq9SqO!G7&*81nk!-2 z{2jk?ft@x$T>&i)*oJ@c?Dp;7M6{z=L6#qMjOH^4w}bEG1Uc%Y@0;3viTa}KfvjSG5=Mn>` z9hp4PoscSnXu%{;0i%!5&AT}#AHu!U2bh*?1J7My(D`av;Ky7cVe}?`;4ynj4v2YF!bb^lto z?{#1vriXOkf5AFJ{xrLKNnR!C@_mY`FEV#d-T;kG5g(@umie0Lt99EyeJ$#V4p7yd z(C)$uWM|9c9ei=&=^JNDi4f@O`G2mQ;ZREnax#4@<=U*7(XpCdkJ!!`28@e<%Wm)K*8ztcQx{km!{&&IFneS3`kR# z>l??7rFF%ouuD&pg0(&@Hq31eTf}OX);?eO1(W>4|A?zS2 zxDd2~GO`SfnR9B%ZVoq2$N*86vw^_G_RS1JmXwbS13JgMZj$3YAkbz3!p;7eFZ=X92=P5v zQFRDKCxcO#N?`!RWv_Y#h1xzcl*@6>qQ9V|I~RRe=$eDSUTG?j#t9$Rx`>i>{zh(0 z(v3xUeW~&=OK>6P>Jp1CC*lq+CrrUa3&N+O@NYZ~G{M6kpCT4q*}YFo*6}XG2`?;! zvzOOh2Oey_MTcY>Y-y;<4gW?|7cSyMe+}a1v=4s0{kos8k-S;;jW=N}x`{w*@%p#q zU_^02JAcyLq~^`~Z8Xs9L9kfO$q-ZKN;b0QPC>y98=vN&hnQVS)aRK(%L9(vleuC- zjgz@;*=-Nq8Yc?@7Yfib!9E`Zz)=9jcCf{6f;DuY1t5VzAPc3G9_8tsxq3iTAYhZp zgKkQh>g=%W8E?*>g|IB?x_P!lPg*+nkcjJ-LXlWGDxE~UoaT7Z``4{Xu$0E-{{K1_EiZFekMOSV8`r-Ei z6HtlVZH|x)mVZp|0Q7k_3z*{;6+RAUc{RC-Cj*W!gZ=OC9d2b5ITF3a0}!z-0)QLa zvrf)D7xd9)*IVc$%B6f7MJ3Z5dKO?IT+}!dBjDD949$P<&dTuri!gzm{qYOXeo_Hv?k}D4hr>(7Xz>Li#I;H%twa@QfrVP$)^Uy=uUP04^vWd00hl=WORVfkJD4 zfxOsMGzXv((5z|)O*u58vRHi`F5==ETP#+ba*Cd|0_5R%7$b`MN_~_gw85yu1)ay5 zno<6<=`tzXvAul2#T%V`O*J4(S03uQtf&GXHF2N3A$Asxaio@HSGdpH^)dZZaxW`! zo{_PsNR7l+>ifKd4JZTcQRqv)k!afg-2kB?XY}7EUy+!_Kd1uSXL}4~&v_cUdnO+) zISPNR>d($U&-~AiffRfpF;)|irsTy&ri_yoj-kT= zp0h??Oz#pJjgbcc&eP!^QK;1UQIt_{8s8Pi92JyPj%s%7SyL!9x(K_0s~+?s2mu}L ziA_mqrRT(!i)c-iWj33cIUfMj`Ug5J{sJ9PiEP9>38L|bu8t`ab8-ymW4H9bU zlv8KS2e$jF2OrwYk*C|g#+Mz<8QI|ffeS1xZw@bS-yd^LigLA0HX-lYid0gJY^F>U;X5KcH-qS zGBe25<#wIUIfD8fdYnICjPgp#D-Eit0m6P-5>T-Hn9qQ({0rr8k$z`vTxEJoeD>CL zA*I28fO8S#sYtlS4(_=rsKo0uKel3!*aj&DOr=Ql!z;pdr~g#uj8A;po;C*R04v{V zgO~yi=JF{>(US~-9_5#cDx0ATL8nQfeljl83GXZ`ZC;@ z_`TDy0;dYe$QDS}k=N*nrtVXvUhx_^<-x7d-Ud-Ud$`ES%=qUq{n`4m_ll~83xEq3 ziS`>kje%>8q09ZfgrmiPnSb7135>PQq!5zH(%wT(RI~ z2q7pN!OIH(iSw&sO4-gH{&Y!Eb@U^`xVNEq%uYaWTTA8B8?TjLvBjw|krncWunRVc zG^_QZ400Q3e5u&@BSlq{6sts7NrUC2t;g2>bJwa)5l^!Ps2xcGPoq>!k_6tu01+#>ldYUu!-y-)J{Ye*i1Q^953m<`nlND99sHSN8v*z%7MpX*Tw}Opk9S2Xa&z{^{wK%`E z_?oLteOau){cxw#2t-U7Le*`j@fDb3I%EZv4!_w1+0o1%J5?F~liVV9c+63Pi-M#O zXxYmG#M)Pzwk{4K8cd`m>WO-*LHp4v~-rwUy8P2{&w*WRqOA# z)*ucCI@7n5Q+Je$6nFQ)j~%Wk_wDE&QIpvlKT#vex9^6KSsinn?7rFw<@|1g2+aJz zwq8J<(jv>Y?#XSYaXpW)PXt&0DeNZqT^;dh1+EJLtm(pRM33zX4`+Ql#|7l?FPt@g zy;kc%VA3Jg=3CHybuPIVXdc}Em+wJXoAfFo)Lkn%O#b8zD|^xLJvGqj#lMf>?KOUy z;?e1?N3){~4*>ypcBd&?-^V`-KePi}SAy7AgeOstlC@4nY59f^}_=K9J_ z;Id7F-OqWlDX80Ik=Md;mE?(;j z948+qv;%?;J8s*+$guzCQoVAlk=I*v1$39kg?y1GWBr1FI~f9+8qfJaxs+KJ_?yaO zNxx3!)Vhwi7~dsnA1K;FX=9V%Kos%fr=avmyO1hJt4A%N*{rk2+=ufL=SWI!Hx zQGy7D@S!n8-YJs>KuB1p$V`nCy4hop8VJb(ngT6Q;|gbj*k3-aIE{_Pe1CZX5Nshk z0*{~-RQ;G&9(Ipq1!Ee5-5d8BYi}d_e3KuLt+-Yu<9eu>cmn{~1^+$JbaKb+P;b)uD8KbFaJTpIv-YwB`1$#P-n%(_+gN+p zf!w_u@{XkG000(%x{`vSfBs>JAKdWYzsKg$VSb;Q9yfwFJs9|}Ny)WDej7#=}#4#NcThHffiuD5%}f5%Jry z7)457BZ97497gB14~(u%zmSk!(9|cGPdY0uA0)>W91B~gpE7a zirp(b`xCc^xXRE>#w9)Sn=D}m0&usPo*s_3Z(v{XDsM{oFJ?Nd*==oMmC;-vBwXUZ z*1s3EzIU&#fJm-DynHwc$7cF(Zz!94LU0q-ITvN`ZeffH|JP8FvwL)<(R7S+Jb+*_ zfHFPqwJS?;jqLHhfeY5$KVAM(c}54<|8+N9BzPd;`NJF zvXkvmmF=0Y(}`fS(@yqS%s8MQxwQX6m$_7D*gLDr>9WkZz~g-*3(RYdt;ytEcly&e z%Gl{FR1*KFSfEVTOpPb=1uNwQ1pF62`!0lE58`)Z!STFH)Jh~$qWIgrh-SVPBNHE ze&L^pcq+azUR3_3dY?YO{}q>D5_-Ed%Ow}|j#g1k*P0yQm&<`PR;VzpY z@}Jaif2*)LcsQmT1m|HLZoNx)_>v`6h>@XC+VLV5*(;E(-cv_Y{3+|=T!7xH)dfkmx__0f@&Cz>}|9_)73U%AE+$7j_PXWr4XIGiWz+cBDc5rFjzDX01e{Urx7A@oy(NPWn9Yh!-%e{XuAsch@x&(V{v=X#B7Q2^Uf+DhAP*0J;LU``iL_ma9KOs#b zp;fTWmKGbesw@uf;H%W0Dzc-Z8b<+=&@m;#DA^A@YM>riMtmwB zO5ygPUQfb3r=N#EkP7-5^Tx0+WO}V`^ZrOAYIqS>4VF3xS=}aDHGE(e&dIn`8!uO* zfa7;s?PX{!Vp-K+5wR-F_adq0v6iNXE)U0MIsgQZri-dY%aIu7@u+Q1!oN=a7_HC; zj6EhQg|(t)j^EY0>vvk#uu?ckF*WLlRa&s^a=RzE$ec%Qlrl3snA*b;-g$g!$41!5 z{RJN`_wxEqFIh=K6ZiRlV_oU=#E*v7#6p+kwRyVZS@n~0`RH@JrV%SCIr}PYE}`jU zN(yAJjA{67s9n*zj#u@Tfa&{SoT*}=%*6DP7d<6)n2pTP(6HHoAuJXJWmKP-DFZd# ziL*!mK?4uwrnHCMkcx7f)~n|;s^LNzb3txS!%Le_zcD=7^j7fR{g|e<=u>6ytp14tP522=fkmI_+#3&(pu5?U^p z3%re6Vf4dnC$T__eUHtSssNM+wQm4Na>mx>*hfcQz14HTE-?5!Dv zh6t$suW5qgd#49Xob`vax8`DJ4X(^->-z>gezbOFvC}FVw%c3Y+Gc_5WIfTggbd_N zE~aqepUsgxmdBVRrhassu(kEfoDIB3-=(rDax>E-x~v_@kUjHF#hyQ>pyC?Treje{ z=jxfS*@}ThqH5w#m(K(#Hr({Mjeot=dl}VMVsi7I79kjRsQIQD?`U<$5LBX&{3vZ% z(%wUOp@EokHx}I3OHfhZtX;{x!&pV0$nO5D`&8_L3RX@gN#9OfKri2PF^Tp&&$ZVB z%E5jl$}53(f4)!Hodv-S95l_&Y}mQ+2k77g|GTvod_zj3Ka?&i*ml2Yp6AZq5MINw z{bOEFnn52r$npIMK-_${7x74p%lhoJgH-5g{PsQ9{!*X3T)C=|z0(L{@B=J{{?~uu z)$${k@4Bt+&}*6Sb%fiPtwc@?X$k{8_Qt=j37>|>I`xL9!F8&*t$}602wIzB%xUIE zs}6JjZrOMs${riZ9+?EmJpFF7a<)vXX4FQpv5hrd9O}jaOB55JSO1YXgAyAfU5`h zho_6(kAcsilw_C<7%xPZFjhXwd!%OCfAt_k!b!Zk@mzKBYFBdU6M@F5&l&~ktYPxn z0D`5w{HD+KwsA^O{_#Kc#c@!ZdS~5A&Ywy=c&>_@C*Fk8RVH2pFOi^fc+=~|H5Ar(yJUuss7N>IY_Ka+vHI5GH&N*P`l>5boNQ>wm;(nU3}4<~h8o{c1?T+Ho|WMwB8 z2KJtnq55Z!oMH+4_3vZ9oO#(7iWL26gTW3bqE~R72I2A|pDoS0FQD~On}R8k{AIO<^$&9VX#sQ`X2eP!W8-7R zu6IoPG2;-&$e4?H)_1wzn4IV{D%>JHDvHB|eD0?~(CdtWAF;0+){U{E364GK;m57*WtZ4TbC8_;?PrJXc(nDx-Gp%kE}dAm59@_05c0>UJra3=%&qs<8uvZ(25bWl-rW| z>xt>PHG=)a>FHIwJ8%@d+{WRWSF6V@SG*C;jM#rD>{u=kwfT*aCgkd-_RSPPoMc%O zv%aRx{mMa}X79b?c0bs^TMG!;&)ZJQe~obH?F;|NC6Kc#3D=}+qqZxBNDI43_<+)o zIeFT|t+#9J=~t|$9Za^u2NEDRblu?4pWHr~f$BzPgBk5VxZyAi9FWb_B%&;L-OvIc zu^mlX7c2CaFG6S3YL=VDJ)3?dwgH``T}-lm5in?Q*?16T=Zkkzj#i>zfxfNK_wC}GfSXR9cGDE; z%&7?W$k8&p;QdjL+akanTDb0y!Q_Ew65B7v7z6Zz*)rF=wtoKwm(GUL_WXB0mNN5f zwHAXAOU}(jFUba7e6b_0$7VV0z=Q|mOAk8d7Ndoe|CJt~yj!$trjQ)l<=w=DoWtk? z2s^SeYy~yIv*xN=z2IZv2&n7dL_%Xa;))nHJuAR@FFy_OiWJ1Q)%0H@--+vpx2pEv zWL68?tpw?hIO4Y(6ZRAQgDSd_tFoY|ZV#)jp1Q2iArEdDy1Z84a@g?wcbXFupQq2Y zJebOS^l^z;9UYdki)|tL3%GnX1d@(;-M4HcqP*e0J+-98aj$#&K7oDRWM0(O{+N{y z1c?PahDM$pvt9mlF8wn96a2n5fJM8Dl?<*IH^1fq-UToO4qAk3= z`KI%^@|BW$h~ZW9UhPC6kWr6s5Fd~Q7P$}cx``opWEf{C@*h%>bc<(PkoI-a7<5yT z!Or^*bbs4^8Vs;JyW$}?UR8>Z!RMbA^;90_($0}`iaM2V!l5>oSyRT!J-enJnyy=F zXrW-=q(Txpts_F|mi&?zIn4;lRjum?O3ac%?uJ+#UP2`0DSC6;sV1YVg8%>=W6d0? zK$Qf=L`Da#up@n}P)v$m`@4mmlgm+kw_;h?_F0pmdEoK9aQ9N&hnht|UIF%~?F`yW zamA_@Uu#R3%8>YY?Q3o18q%)Bhn<+_sAjwSYdGW*6fl)-2d{PBC5r z`o!_gf7XyV_{$E99oun3ElwSb>6Ddx1hBzG4w3YvJTOp^T52dxDx5zaZLQ546=zZ+ z?yz5nZZkHY%xfN{MU9yrSD(S0lB=}60^0m=+lz$#CdaCL%Z&&m&J)VpZF*BwEdKPC zspxopupd}_YWo=7PJy&?CdrG!cB5^sEgUE?xOuLfgwgUNmHx(}slp&|`N`MTQ{V56 z1z=}Kvq-HY4`I$bL#@Ph^*C=gk?tYBLNC-acP zk-oZ^Z+_zUoCen|(@_VDLWtxUB>QqV@EVlCTS9$>$hDtaWjk#|4V!jsB%^u3`<)txD*Davy{d>zVNcJ)_tc$^tjvkm8J=iuUI)W==$aeH zqo^+OU%#9N?5rQ+b11ch^u|%3O@DK#x7z=A47TPC^HX?g!ybLt8w~iFp4h^WqG0HG z9^a;Au2|K_RUddESz-s?Zv;F#uhxlqQr<7IdfN4ycY}_lHm@R{==bHFF-AnFj<@nh9JZF=X)s%$Bi%857_QaNt4yA{n7@^O%0{K^AZGU9|ymb36r?=499{7x1b zLxNUzd3I7zvnOTDmJN>wG zpGS|oj;~K)5kItC3VPjq?Q$?#t2!xgT^r2R@HyT<9X)Hz?+^N_!YQRe7PktURf8QEQ3d#K`)2@bezL4fF)!C$^zay&S zbsb+_M9`Umq%u{WIis<8z6&P{J4ZWuc$$~O7Tf_E&kS$Le$hj%J2t0-jTnDD&yW06LAJ;rsWr@RSo8| zYF+$S$5YOicRUR?l|~mhn4vYhyU%)pU$54Aw3>D{@?mgQ;1`PSZbcZI5S8`4J2L8) z8*LZWVqkS1RB1a*&_t#a_QChA>~bp=h!_>WH)}1 z@*9%?>=e}2?;g24?@LNM7sc&zWXn~GOonhz5KV5Y400@o#xILJt;MvP2ral_{EBh= z-obBUJVj2@RHsTSSv{x{exf-{nUx4|FeQ^5^6V=|hNW{*Fz4-Gr~b@JeC-NVz+U-W z>tmGeIZ4*^3+TPhm4-I={f1UhI|)aoN{D(Dx5s~L(oA*9T`GEnu+gIS$(-O$3i1}_ z*RYD!XjA&*aYtO>o_+7F6WZb%=>Jx)M}lkS$9bDY(kK!OvD9M(nI{_OS$sH^Fh_p} zAEl^RAeRQ9*wlMS`L+rP$9ty{0`TqP%O|&3iR`Mfd0r36-`i;QcB|f_rI~ZSq(Tv!ao%g_N;%O=e%H5x1M#R0> zU#)vru)hDX=kiySI_wFh+Xa2yBx%CYe4O}4lpi<>xY5bCWBXB>=Z%InDkoj%^^tqM zQtS^(`ogP?UB-%+JZEODdc+Tad9OvHd|MUce4_8r8;8Rp< zyO>fwF#VcwM=0?wz2V+YlD=OHyPpiD_@~gQi@HCeh%$MR(Tz*nC~3^7ojJXhkHfiZ z*(1}3=bq5cNx8hH5zMwsWPZ#P%`L?PJ>jLZ+jn>k7JR$(p$O{)vU3D`M-TlUShZl) z!=q--%~|H>6w{Pb4VJCsxA9#EsVUJZG6JAJX z_HP?k$%l(=r?TL#Q347w(*`>>HrS2PmuDcGvAbw~omjzekm?BE3A$>d8!>2c)uwH9 zB4~2lwj07~O%={tIr?}Vk`?!Z zAbM2t*m-p1x;jhEiX=AW_KH&AuF%EwV`TuQ?;kio-rc=O*wYA6J7Ly3KM(s!A?Isb z0b1yeHL*r{~AH zZ&orrzXy@|zgn#uk&Gb)Z_ltJx?D-@nZmZM^IZB!xL^|Nzhxowz+!i8i-5KGlI;7n zpI2YLNx9!$D7W%HYv$P+VQNi@3;QlDJ{8AD4{J=b4_;3-4%TQiRn+1W?8xd2(tD-0 zJ88$}K7S?ja!D4eut3yr^4O(3nSOs>e8jqVha-`z!uF7QjV#)$3AX31-SX7+aD;i1 ze?7Jo2|5-{04l2z3;>U$emr?Dl+Ej>zdfHQTKc?LcZz5*ESQ* zgpxzTTkP%FE`L8)06`VfzKp!RT9>eE#1<0KLBFRo*_5QNRZ}z*OZB1!h*Sih{|O^V z5={WfkMPzE!3l1=dCj&dNF+;|^gm%v+btLzW_dbP2rv$Je8vz)ZqtSGYiRL$48a}Rh*xyoi@1HnOvy3l zeeKFFzxv#ipqa;M+K1AaT!-3kEQz_hB(N}6FCeLe`qfX(a|nz-gbG@7%I5)Y0}SbX z0#f;gjkv|F7p${d1x5d`@FwIMSYeT`fDP{mTxZpoSToJVa$oFjO&qJxt82bp48nqt zLKeyfDGgFK{-_Av|4N<~a0i6}CQhyF+n*q3KNM3?pwE>)!fhu6Uk&Xm{x|G#%*vOT zkLCvZV}PZxd9Jgz-=Xh$)EY)fJ)@?j+Uu-simJb3lduUJWFBJnB2>u#!dz^7Il#h~ z!`ptz?7^D-rH^EsEtVu|4*nmO!~-$9?iW@ zc$aA`_O5H#X{mA<^#SIWa}XdgAiydYUK@0SI8&Z)txF1Wc%ir(kW*45a*BvQ-#8`? zG(?A$oPK$}a9);Bl3K}wSrL7hq}-;-3jSgG&D>>31*+a+`)5Iq_$@JuN=wTF$N}i{ z$;W)_Fxga$%%cm;)auk-Yv5{|Os^QwE(Hiwa7g$jeAL;Hsll=m6l4B47V#ZhS79;% zxa@USD@kOFyx(6K{6FZhP*-Il*5o)>r`QqPHcg|9STd1Y9dIEdH<#R;76={iW$=8K z_ACpUqa1KHZhrjDQk&L-{_rITR8~wK{zG-rIcvwREcl5$Z96H) za`f#WS_Ds?)?)0d9fA%Rg{`F+8RIcT;UMqupk+2tP-x9BC*%naTW52F=h+{!0Txe= z?Xzbw7N^PXDzu6my5xAleUZ0`JqLW#H##a3o_39#WS&uWZ1$y=X7S8?p!_CVV86F_ zn-P`yn%;AG$>D3^;Gi`VzEFFi^ce-v=vvF$O-p*$J?^d)oy^@-Q^amNCk0%`;m!~s z1@v{ME>Sh|_%J;QyckbMl)afrW?{0e5SR;Bs6#Pum|yfa{uPzH9t*A2mt13Du?`Q3 zpEefu%iHqzm76wR_pU~%8vU0S8Ddf%!x?n%Rb%JrZg@mR1e;r zEB{)Fs)lted@3YZ!H4u~^jeCYc>x#Zbr5zN?QF!KHEnaVm@rl_p6Em$Ye`YJ9ht0~ z`n1u|P|GwSBVbrR0g?-XY!#kOPdrEkqdcRTPUDBVyENb}WQj`ws^&%;sPa zq+umMln0Y?o)oUEZQY)Lue&*l4m7y_jWSR)D++=^_asE)`8FF{#)kjX5^L~ujCr99< zKf-HJr93nbM`b+(a~rF}PRqgYI7Ni$Zxq2dzTgHv%w4#y*GE^NypP3;=`JG!tMco^ zroIhG4yeehJ64C4;29JAa2q)Dn)-b#B~SEzK@_;m(IKWOu+fRQ8>whN!wZ24jNQZ_ z2K!a>H|G2%jeZ!8;2SGLe=SkRr#*j2P;{&e_MBy3pg(x_@r$u z?yKy~1n18gB-whG<-PB)q#=I6kr9B-XNib}wyeLrIF>YlWtj8g^}h_ahXbd3Ar$u} zu}n$hBCf_!+0pZM{aA?+8KOEIdK*I;tt-|9hPEzI<1lffhSbe}F7(m^+SqvKBL9!c zsW9j4poHaq#{F<}X?mV&A^6#*3O{{@?H_-BmA^OM7L`k`z6Ax|fMA^d1igCHZgoxH zcn6E8KJvnJ+pzdjx)Q?1W!@FXX9j{gFurhi45C$xBfuhO*gpH?@Dw)RlQHUndri9L z#9CBy%^$2ogfP+o%I4Cxp%t%%%s<*xo>Z1JSf}#MfIP41LobKqYEoo~e7*u3QT{sq ziNEETpZQHTgW!q9anjZD9BOvK{&U`P=pMz6wYyVxgCs&TN#9*I9&}h~+pN-rNdJE> zz?9F@S+^d?#h&Kk4q1Gh+F}c?s-S%wA7fT*^F8k`kdal@u{3Fi*xV&(b*Nmz(@+It{b1-`Q63NgPE2v3f1j;D0<~ zm_05vmK&JlX?JSTLx%>2_Od6J8fTa_{wU|efj2f)?#&~i7^f@$4%_^tAPc%k#+!)P^gvf+9gjNeKNzKI_ z{};gA^3AN;z~z|CEr0y|FLapstepVvF&QRGTeDsv@a%u*UsHnX9_%`xOyBV(v0ygu zK$V32?m8FPrLD50{RtlbyKFE`Zwh65BaW|T1{ggUsxTVaJ`isI7$s8`e0Pk`270yd zu02`9gVv$s<2VPN^hM$n8`=RO23LHJ>WHTc{F6t3@2|bK9E0s^Ovj3_uDpO{4_4*h zge~SQuhs^m_csLr_LA-{_I5H=5H1l#In2L_cB+EUU|Om8EWOuP?N=G-tb|=;=A|73 z!xYiad)2A&n7Q%Y+8#_)RgssQ31tkrRoCnQGfLaD0K@NI+^OKqxavRGd;NXU^b$)_ z)6IuQ=UtbChsMAg=?3Sr2v<9botIx=`^|l=`jv2?+~nk)j35yp9J^GM=deqb(ikiT z1Y`+0+!e%pj}wbO`;;Tto`U<=?v2^5`FAp&spJ8k5P^c8@SY4GGHOJ7)s*SYgh+H4 z!C6~07I5ADNKbK73zzytRgi!Iv7Da~q{FUhxwo+P?R27B|j!o&r4Gz(NQ zS=2rombloap;WmCrk!bHN&LqJm_g(2cg}>@YBaqCAAjSc|2k*C)OfP! zHafo8Tl$)X>%&WONxY(wQbqw|@>`zJX%U@$$(#yhl4Tl7gZ}|@;hor8sZBzgHoi766` zQf~#BY|=uA-nW;^cG3`0;;jVBItC`|w7E|5Z6=g62(H$rNPCQ#58j=Tu z@c2W(5#JgW|LSo_zp$CX6D$oZ#qkvV+dym0!a>=HnrONy!e^6(3q;{`ED z8d@6lJpP1ZjK6-M^8jb9FC0HFi=pfG`i@saA$p(^EMebEc&PYH5$?Jb(t73$U%o5C zHzrWb=!l%Huqy+%V@^xVmAD3mwXM7-$+bG{il32)!lcY%#q7ch;tyEs)r# za4;#*BZ;S)L$ra88U-k3M@mzp;PsiNtrD(eY_v}pL9JzS#sT=P`fZ1mkS&>x&iU7m z#OM;}Gk}Dzi~VLpB~~nr+tI3@9+@#&a@C6#YtZ2f+i||1+vx9%R&HZPbt3#tFQj4* z$c@R4s$>uInBqlaX+C|}5yjHa;HA$v_r)xeY5)qyC45QP&lorlK{p8cP4-gq+kE{l z5sP1U`@MfPo1C2ZxIm)SJyuGrO?wU78_Y+KFOy@jC(0e71gBS755AZ>c|WSB2$4Cc zbQ0Qy&q9KGwJAod%IkVY^Y2@Jl|Q3$0HeWbEu``DB5NrR{_&|1|6?|$XyceWfa z6XV&HRab09hjpd+T#XL%e@!lAh=(E?JP|?^yfoWujI$D5GS1o1e{vfyt0GLW(!71z zI8m=8qZ(b-KY5A{>{iY;CnmaDXFV z!naLpWnE7hzJiEqKnVTef_+}Fl4?8@OZpp7|LX*p{=FkvTyCj@NE^tWX-XEo!TFPR zdML&F_Nn3F;)y9)GO6}w;DBRRO~m_IHO34Kgcxdpa8v@^TEK{FD~c}$w}dRzD5R@M-43}7DbM1y_UimOCH2g@t3k_o*!&Jl10uF9UPmF3r_BxnF^ z+B?PXs}H^yXEMJc*X9>(zB$Azmwm%ZQt4ou$5Ip&JAJ(n;9oKkW4_c#7W`91{U|%+i#A{!2ZuAS)YZGSZZ>ke zxhki{X;h}(O#)EXlW`gu^B#k|kU|Uvf8JnKjH3=EyunPftAwhlUlV|n>j6bM?2}K% zoGA(zJ`>ugT$zP-SL5ao)^-NzRG>(9Hh1uV)nl0#kk(t{$;cW9dK4!d4X@$&E(_?4 zp^lwXz=vrj^_*uj$W1?|EkAFwh86SY_jFRbdOwX>F0S)cveu@zcXnan~5QT&%M4>G3_IO@a{De2OH1fq1Sn(U1fylC~ zL{V2h^z~%!QaYNLaXSGta_iY$#Uh(t{HU=mZXSI@vW@650|+v))7x^kI}}4t(Q&#U zn;eCPqR9Wwi)lAiqOz-y#}a<3sd6fUxJ$RT*$KjDZB>T}Y&0Ur_|Blg*IqvrDZ{*> zL&U93+?{V(9M$WhKA&5eR?_9rWpd!35Urr1$@#0KS3~Z+xrK;eGq~?PylpXVXfhV(QP~HLcGI+=6;ku3kfDL%z)Xm)^z|T+BFI z<^*p${6?T@r5Ww4OnA*W3HY-cE$8uQ=Lk818}7{QF!Z{F4Ygm@rYw#EQ-i>iOE8iZ zGJv$?ZEv@1V`}Rwmhm(@?KO`bmwYunWkLG^ZhAL=DN6CTD3vI#IuK18O{3zdjViKz zC)w|!Hf2g^6Tw5RC00w%chcGiI?^dF;%>DS^hxT!JX20v#*}m6^(#L$@f~-MI}J7( zR6+Ty3Q{|Igb0hAK@q{X+9Ak=I8(HBYk7Bg3n~RBWuX{}&K4Tqto)R$2J?831m(QiO2@^%-E?+lDemTzrJaS%E7CyaEhglT~&z&~#%Q(4U zRj^kMb_lKuBq=TjzS}!;tt5pzh9#r~<&}&g17B_`XR)sx)%?vhZhz0)_!B7NAFgRt zqDqNt;W+>N06jB4c1x$#ZNt<6(G1L)Et6~Yxq>d+zamF}0$uFCa*#@P8ofML{9R7c z_-l!VMce}#uT8O zL}(|I=4k);sBISwm+H4cm6xFNB2C+)K$5YhyW*fALJ1E6!6%Z0Aiy`FxYxlUbg)PN z7%ts#-s)2{MKfY`;X0*?4Jf9Q= z_XR}O&N@TnH*5~{Hm9Yq5fI_;N3fhz zT`-_>{gW?M{3h0tAML_R>pwKw)HKk=+6?Mj7_TK zkfWm5W~L5jP5(sPvCd5n!Vqy-w%^^(JN1g@RlaH1JvBt;v$yHXc6HB|)`isT3@^s# zK~JAsi6#fCFFS%-e(vb%Y6q;6mI6#9IO3tQrBD8p@OU#b9A!JL2t$krzJ*8lOxEC+#eshqfL!Pb4X6t@B`=iBmZas&opPQJQ1TOM+}}d>h-uHXhQhbY(eBm|vv@ z)z8)yVx+NBfnI~Rs#-zy`q^2{S-0Qsmo1g^cmGmfIvp?GMEO=Iq>o%}rvD{g31uO$ z%|b@xQ;=hYdl{eG=NJ&>KMq6ko(Nx`?!C%0cfpw8eCIY4J|)?+U)xh%;Nrvx_vRO` zm?D~M^8U9F*Db{xhT%jI`GmPnvAU2h$%1!{U#el?W-VuOq6Yx!##I}xm}y(QiP6dn z-J|TX*V@?R=Ffoa<1c&li+!%usopsJnk^G_p#pdsdE%&KnPYSaHJ|!`=Cwgeu=boj z3XTwi(x@tybqqfDd1`2I7pbwcUVfd-OI9(S^vMSo)(W^^pUflr`t^@ucwvn37lPYJ zXckH%+{{eudQc9TbKXb%;3+EkZfNCuOEa4kflIi29Y5?)zweHQC15m=%c1~gDcw#V zRiQJllHs-53&9TucoYXmL>NMkvr*KVCv5qLFc`<*vKxhW`6`=Vj(Y8Kld;E=YYqWg zRVg_U1Vbuk9A7Ob;udW>Ecnk9t2!=cFNjH6rhi=^yKD_jP#8~8!ZsjBa|#1@O(}EX zUA=>MtGD#D3NkE0LCXDfep>Yv@GFpfwQ?m%NeTz=`7c8h z4>~+$s3uIvIDNv$*$RW;XCW7y%q$4ZTMlo=C zF{usny}Nd5fp%;>SvB1q^On^1Wl?wsO*bWTuThLN;g!3>4udJSU^_>uh(h>lXzVhD zYL>4ZTOB)fR0F;jqi@uYQd}O7qmsj5C0&|jBGJ=zS8*VUOvXPe6g<=gF$C~y~N$_Xx+A)cmTpvs${ z;-<5eGw!-#cwYFy)HCiYj6V*@(nUUZfb{hKeu%4vsd)(je0lU%FwF6&TlDtz`2K6E zfqbSr^%95{sp!~6JXFVx5b#+Ir_5TYYHj&%DA<$Mred|);Laqo1>z{nfjxtjpk(tF zHgqI~_#0FLOqUWbeF|w-Yy&P{A&UY>r;;Ui!waT=^X}_A7(da_3Gk&xeGBHy{+)3H#tuodnkWiR`~i9LeC8u~Y;s6T1ulEP+Aov` z84~h+D!9JHD>-jdfb}@)@-EOuY85FrAQUlKPdpF_vQ)ELUw1!lcn5SUgA>g`NJt#YlVl*5?WjWPiOM{9!(&iBpgdf-6IskF? ziH+AJSa^+TZ5Mb%hf0-Zy=Htmch`_(H7ldS`1oWpPB8=OF(-ai5mKEvZ7Uq}fmIX>Y>&k$2KX$1!W`SZUzbZl{gi>LB82 zy9$Sy&IfI=hqqgI4{69aamC=xCgb*vrdhV^9Y-t`G!}#x&Q_rhVbwP8cKs(l6XYPm z-6g*U>E<(iqb$|oxjYc^RR>HNS&MQc+6D6VQw*F$$Fs}`9i;&_D2Sj9rWc;}}_%5~eVJ^Gi%;vmj?rc}&Aq3hIJ z1)4y^4>V-N;x1PAZ}>aRv8_nSP0gvtF6inX5t>^y8=>oNvFM;68_0akdr|UU!4o4a zB8-v}pHUuBxz85*dxoW6-QHu-eA7{6(`bY`4ms@@@td9AK)1IzLRo`rr4kZx1dqOQ zoD4)O9$t{dqRZXv+o5v4oSzKCeDYr@85geJJ%%XZI$;V#Y0{F zdq0-zrM;C%Vva~r?>8^q!&hra8@Z!{nW5t*IUAH7Qk|BsMXWvevl8!@{;dGw;ZEoV zeF-6G2SZBj0EC*J`Bhds_>Kyam2-k8(z@0af)F}v5p$k88T(W$Y*nFGXSuMndBFto z7hs*6$Bc#uM$*nxx28F)#cmVQIzWba=yA7wRO88q*P6F z1A$(5yxo+%hbfNoPX7)9*~zJW`BTk(7<@J0us=Pr-&+!hsv2CTv!MH=;w#Tva5PqM z^ak{DKB9(D*AZLED6|2ZrcEB@o-eCbzxIeo z)VHDzS)kZMgP3Radtw10_(!Udp&-vNUxNpEX$TYwDq-GS%K4WJZCm{rPxx*oJ36CL zAKZ@OazhuDc&ZM)`~m|R>t}L*0D%FRpaz*IS90Sw3oDP2;In#4Fxhc8pFvzTe2)W& z0@$>unz{E_SZLC*_-29~zxloU3S&e1s%nP$E`?}_B)C8{kRcEQK$EC8f16xhGa#ZL zk~Rd~Y#6$vyjrEgN?&gDE5f8z+Wva)UABUS3=Uo39ZUTDeKR%R+12VbeXd8;cKvg7Hk!0dGYXUL zL<JD-U$F(rpiT%;y-Q8i40qD8%IUaF@!MbBcXUpLA>+2-tTBw z%VzqTw6d6B_7@?Hjy zEGQUUE}Dh+>dwxY~%m0>`QTT#0Tl=qGWT_czoyt`-&KP*@ii zv}bzJ$V?B*A+Si^+svj9ONOS2h5W5Mhs&S}*3{^nBR8jRtn*cK4G+CZ zMz&}gd?qu<@Hq5R%>!8?7Em@~8#4MCYZRC>h6-z{Rb&XRBVFLT>Po6d+f>0q^Pc1@C37>}5 zz-t=^tJlDJIzoCB0sc7lb9GbPmv%SdJCp{J^{<(|B-*R^49Una| zf9XP145D1N+&aDlRi+k8@Qz{O#FrNo*XPc7nC;oEqKFcX2{d&l63ZC1P>N&2Y2{Bk znz|m!Go-_c>tx7#MB2cop~$??Jtn)HuzfRZN7QK~q5 z0Q-iDLD>WwFc%o&i(a%e943VQ?M*2KMW!Vx%8PffDgbr0q}A{gf&>RmwA;jl=u~Il*1AvhE@^)E6}|n#$iWS2 zZ%U?rpY}I{Y(3C&V=FrG@fenoP_8A}JrFPnyTl+^cRfHesi+#Kt_9_!xPPh^rwdSe zj!Ghkce3hvvp9q*@^i2%H9uyU?TCb*dDH0&cH1}_E;t$91RIJ66sdAE1JkHw= zRZ8ftTlZ_nMvJ7N0cf|n4gUu#qRfWOBGhx8g$Ux*3XT8p~C?S=a}Ke_(- zEhj{&r>_Jv=9Dou5UUk>>}-gnm;A!&#*S`4HrMY+^7L#z=kfLS&0=9VfPL>`?JkW< z453s9m;?F03t@U;^-mMg8ArPYJdxq#0E+t;Ya+@YK0P7d!dD%*qC)vN-FD!Z1f>aP zPI+D-)NO<#(X1*$!9<6J6pW!cUZYjQl6hUMmQ3Ac9??|1a1DC;CTKwT&Ven>&c& zEgKcbWXzT9L%|hpC-J^A`IYBR$d!2105{jR;g)1j9t%n6oLA^oO!!qtHZ?@2yq;Z~ zVb%$){Tl1iC%E7(71Focx?7ci&!@rG>Ypr$O- z83f+b7m_KIxAc7kZs2=aI2mByl^Z$Pq==U*KK3@;@FXV9xv}RKjhEIK z$>&~qTuJC0JZ>U`NW9c_>B{!ryFHAf$okc|krddAD<>^JfQZMt>>R`3tz+|&mfT>d zC+EtQw^CgH#H@@*)f*TwS8QNxxe6~Cn*5dVsCok<=7x<%VD81h_0Ey}_wT}?hl%;l z4ae>NJjy`X3S7XP=Eoa0W(R#Ape7K$$BcSq_@=N_-V<%Zl^r8Y614khDRDMXkq;NuuCxsOk!4qKWC2hE*K5;&o$-uybx$22&kV^=1L&qgJ zG0i=y`5O~#xEe1RiEHX5?YO}tSAhE_7=Lq#{=8uw{)(x)w;^U!M1j4?WZ0?yXz_te z^nHf&eFpCff}@_q@s)yXxZX*jH!vlx-oWI(Ff&GCZq$ic;u?BME3QQ;Xft@b0)UMS zw1V&$vd&{`Pu%TwR2UUk;2>trXzka&v0;Qu?igY|n28Qy&$FqOlP%<3&5}_zT<-+Z zb4|UZ4c9EeWW)7NHa*wlB`vt>HE28@HME@}SK}qi;eHNaF~AF%sP(hYX+b5ttO{MO zz(HJb#*2M6w6~39@F6bIXaM^&u%&ha2`r$T8@}FIJ~5-0jpUQK5@jR3kqaq$Io#l} zs7NWuhATFRGOpUd1m=^N>w@qaTw3lDF9!27tKZ%8pK+~aqoV2xEJ9|=itRhs8*^xPXP1jEAfBk}9K65pKz&$_-42 zn`Pj?0A6NbImlVwj`oFXPaOIwY78~0z#`0OUc4ivQeSq!LtTgVV#3~F3}IlPjrUbf z%nBr~tdm^9?_>6;YVR69+TO^PSUo>b5UY*P+KMupUgOp1E`zPNjB$OFNL`?&yNHXYfu$w7m;#;{sa~uoV;1 z(oj#T9PuTtwA`Q^mGY<+DFrFGjE|Y{5fMIe3GXvI?-Tg{vf%faQt!Bq`%a%N-S<`= zHMAX@DK=vTHplU2t=Kx1;;oozYX-J~7==v$Hs+PE z*hWt2z|0%SaoVy#mVmWPXm^421o{l%jV{v$BCPI&yOH5+q_ndJZs&$9*RM_YS+{!m h#Nozw*CepR{|BJ9Q4FJMynz4!002ovPDHLkV1n3@N+AFM diff --git a/Emby.Dlna/Images/logo48.png b/Emby.Dlna/Images/logo48.png index b8fc145647099cc15f8492378c17ab555d4fb163..d6b5fd1df1adedfbd025b73121436668d98cfd2f 100644 GIT binary patch delta 2252 zcmV;-2s8Kk6W0-tBYy~iT*gHmV3->-Gh=4uo*Cs5 z*$i7`fJn*^K}H3BDn+D<{NNS@6i}3LsX;+>X{Tmej8len(z1d~LFaV3j0p;HaTA30 zr}R7TJNx(ko%37RDDWgF=gXJA`QGO|=YMZSdmtWH^W@a>1%F#^Ek9a3t>VbW=@s+q z-k}{}aE`J4mR+6u9o+r00xN~=DMT*;AMASMPcN>STK1KTnn20;9i0&2OF|tXBm_Wm zCm`Tx&CJ}neAcec7bStMc`tRS&CJ>%2Du7Ci28;4U76gkF-&`5b;0hei;93knTD*# zmB*wR@6p&=C-pRK`9md5rjx+n;gPOca& zipAo0)>WZlx8VInbN7z8pah=FtLQF9o)FRq01Y`S^sAut&qgBHUKFeBaX|>np?xaZa{r{aY#57i<_jVZr)A69IF5-8)Bx2Ex|xp*eRQxcs~jP$(lo zv{;Bsny*bO6+R18N6y@_st?#BkcrZA(FJl1Fvvh8%**p26fK*{8aDM~n*R*~ET zYcRfG^MA1Y2;shUxo-lTphVD8h34Og{@}KvH*RiM0#8k@>`3YZ0{Ekg;lkL)p*xWR z%O1|%A;hAPUDFDHRe6O6IY;GuI$pfs=iCIxN~(k;>qp1Y%X=rcG=Jhn1- zn>G3y0X?|^H;!HU<`2&afs*mNvsCp#p;HP#hy)GirwX_C#rDov6x(wfQh*k7m8cfD zMSq|g^YEBQKFIjL31~)8AjB1(1_ffUeAb2`|AYte*q&uVY*NGG!aHAI0uMH=_}(F< zu2K@7{Ll|4tLL000k^{ zGP|0E!&bbP^S>*QnRTZCU4(=>^$rVeeShM*QwZVy*?X=K#X3*6U4O^?H~Ki`_?n>! zg)R`%%?(DorK5Is+XVb~zl$njDH@bl^EVIqt?%AjgDpaKln`M{bkZm`duyv@*E}o4 zE~Q5Wpm5M5A9Orx0-7Ea3bm8BL5XPcPP?bScajj-`b%oSQb*0W=Z$gjKq4q=QGejI zwCfVBNyN^Iz|)h;x`CXXqCt6ic3q(s!m2My05MqEcycLh5hzyQ9^aRZ5T3txK)DiS zLcq-rk@>@yzt#DS38Y0L3x&w>H7HQ=o__V|#9T{W*1Cyz>wgfv6M>lp4+0~L-Dn30 zwNoO)%sV3jB~#0KS~B0$pg@Qh@_&nmS2|HAzw8nP3&PGi&NT0LkG;!{E-w~+w?cp& zt;shjI&a{k?{;mI0E4tS0%i+=@B+;P@JiNUwk5x+-X4>{B!RzXh|r*_JKs zIwnMCeJO$8-M%k7pv+XdCkXPu9h(N1yV0s7BG8%5A=6dC7vllV_svUpE`R)XnGkO` z5N;j5!8v`m9L!8fpsFe{RcX2k0nc>pQF!pzOZ$uwuy3fqb6QPx6lK2yk+(*}15s`W zDcP{Jds_9Wo4o`SN->x#06f#RV~Iqzw_p>PA3I^wS%i30u*8qxfg-w1h|hg0C>Zk+ zD9zjTeURT2dOl?EOpNJB1tU*;2LJ#pWqKmkgWd;FP~R?=MUNzME(NuhjhGxO%&?Ip$r{+m2}Ej-{zp$7d3Eu1 z0onqoc=(-H1jzFghL;$Nu>iIw34pjM<&O<~{mlQhlyvM+a-9QMaE}ssAr%@+^l^0r z%$B8$r);(1tmGM~R z??p|ijp7f#SUvDC+5rRByxUidj;tldTVRwJFg(5tRlCEY0)O^yC6Px3B`C@<)?kcP zV8Q%-9}D%}TAD)vaYzh4RKvqIqQ?U0>C=()DnW3{WJD9k0nkdeModOPlcOrpT1q!- zB9{r#J0Sa7gFe=8TygKehs@sAd%GA?9Q$XVYVOsxXE3i=B?zp8#) z(5KExfi9Nxenrs(cxk+M1X-x_7@^kNJm!pG}-pGOYLCGBzjWG z_rze2sM&+4Ew7r={~$t$!h`twp??QZx1K-}%Gkm`e@)d~snFgEUM7(4mgZ6+Itff? zq2>sgQ$NFYQM5sXz(J4Hr|;~0csbtqu9wYDRZw#tOTs>mE!I9)<(%;V##-Cl>u(_gE5Y>hmEMt0#5_Z5=p-^#KtAN zyj2G46YqO{A!O9FNeQLQbOclapapP&&zjp-qt=hx)ql0Vb>)V`)qU7jXr8+10$=p6 zPAGmRV4Y;%1boSlg-k%hk?IT=+;Q)ibzd5Rr|UKiNkL!n$zfTLeh91+;0uU-EUKgL zSUUFO-eTx=3>b{Q>En(Z)*Z0fy$|FCv1MZcx$TvSk>fgJvJ!_854|Sv$Eh2yRbsMr zgJlMI27esrjBHksCM8@qiBNV@^5Xi&Z9ncUj$Sc;a(c^f@nf$!;ebGn5AO9(9qrQV z9~%E604$if^}LkW(>`%bk*&0sh@ZY};pm-*isg_!U@&@qY^4$d@>6EF7e4dNCS}0` zz|Zd=^P)i}8@VUX3%RwGUE&@(R4lzB&@^?^G=C!|2DeNg$NNrD+puE%E8%rFFROkT z;17)OgRzi{5&QO)^SA%#h!S{w>Xw0|El4X3?`6P*bL$_fDVi2hK;@FU^2=ERxeiC^P#mc5+*pomFfIL!PMg*{(apG zr+?9;%?ab_93RO0ywrdT0|hA9?>s$o zWzCa4RhS+3FRWhe#Qv-iovRWTU-GM$zkkyw1e$6$mzmjb@={+;QIf(rQR@uGo9nk; zJ+Hp?%3`wVaGeny!EB~T8_7hLUUcs}rH4(xO8o{UMugL*f>6Po*FHGz{it<``qo>O zSZ>0VjW@h>OVpg@cb&b%Xp0J}bhj?EsCUeK%^=3t{37}uu4GJ8cxA&0^Ha1-G=!65| zCAZCL9ifJ05!Lp$Gv{nSDLlVrKvh>`=t4W1(^Eru0Qx__R$^{l6q*pQbXvR(ih&$;)Vfrm`MO8iEpRfbz* zC83^0?&;fRZM{s1^P^j8z|kgO{fq5WqSmU1`I=720TO@_N5?vo^~G>T7P2QUYZ>f$ zv3H%YBI1DZ+>8}9-wLl;)Y#U)z3aeRK3SFRHvK@?nU#?ucf?EdwB?DjJAcA5zI%Jy z^FA>lP#`m%tL*hsZp6lUUoW`Li1a|*^X4j15p_U;X{7Z#x{}v9vMT=&U)cYHI5FwV zwNWyB(VGgh9iX(zWT{*qwWh!WP19S3s*C-{k>wHhI?v8rQFC#4O=3pN0L04QO$m>6 z%s)Qnc`zsIll`4#CqJ`rVEq*{UJ@(BFpC)>(> z>NTMQ3X}xMwwcSvZVU@(s%`QVc1^+I200l~IUcYXiG&pxaNXR~TYr>zEg1aWIKbJ% zA9=4|Q3CDl$>~NpCU8Irg1IlsV*ft=q>B_BUU)zWq{6T#(sNH~^Ip9m(hJ513tM(T zX+z>22l5U*-2@Cq6+p5=1y59?RdY%5AYY@z}8DDf{NUDz9z=LJFmXT)nNwtpn08tDN5*n0gLTNKz; z&{~ji;AMj!PB(cm0O)Y>DJm!nGtGQoN6?BXx^wY4Zv$umfJCAt_EECpc+iG9Pb!?U z)Omo3b;5c3;iY!}&-Q<`{-z0jQOjosX;}LD^9s~OG=>Aj;SvCAV(H1av}mw_FCA1_ zW+W05d;mK#&wpVL2eukf7xqG?MV|s-jXW~F+3PjqN|i$N_!(k+2IpARPccbKXjh$ z@nKzEUP-&cKOm*C|EzzoCC%<;Ygq8mjluRVh06VBt7PS|J(`R^aZWXq}58?#=X-QL z2|Ao7oj$nViTO_Wi2@&|h~19ZogzQ*JbU|tYewzv39|nKTu58?f~7C100000NkvXX Hu0mjf+c4ez From 09505e0988ec53538c3ecc3d243b1d1a5edac8c6 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 4 Apr 2019 01:54:31 -0400 Subject: [PATCH 0310/6208] Apply review feedback Remove a few superfluous/testing log statements, and print the deletion debug messages when it occurs rather than earlier. Use a nicer name for the isDirectory variable. --- .../Updates/InstallationManager.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index e5ba813a6c..95fbefe00a 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -85,11 +85,9 @@ namespace Emby.Server.Implementations.Updates private void OnPluginInstalled(PackageVersionInfo package) { _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); - _logger.LogDebug("{String}", package.name); PluginInstalled?.Invoke(this, new GenericEventArgs { Argument = package }); - _logger.LogDebug("{String}", package.name); _applicationHost.NotifyPendingRestart(); } @@ -585,17 +583,12 @@ namespace Emby.Server.Implementations.Updates _applicationHost.RemovePlugin(plugin); var path = plugin.AssemblyFilePath; - bool is_path_directory = false; + bool isDirectory = false; // Check if we have a plugin directory we should remove too if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath) { path = Path.GetDirectoryName(plugin.AssemblyFilePath); - is_path_directory = true; - _logger.LogInformation("Deleting plugin directory {0}", path); - } - else - { - _logger.LogInformation("Deleting plugin file {0}", path); + isDirectory = true; } // Make this case-insensitive to account for possible incorrect assembly naming @@ -607,12 +600,14 @@ namespace Emby.Server.Implementations.Updates path = file; } - if (is_path_directory) + if (isDirectory) { + _logger.LogInformation("Deleting plugin directory {0}", path); Directory.Delete(path, true); } else { + _logger.LogInformation("Deleting plugin file {0}", path); _fileSystem.DeleteFile(path); } From 754e76a61bbf4bbda5a4a1073737c07bf28f5f3a Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 4 Apr 2019 02:34:23 -0400 Subject: [PATCH 0311/6208] Add TODO to remove string target --- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 95fbefe00a..6833c20c3b 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -509,6 +509,8 @@ namespace Emby.Server.Implementations.Updates private async Task PerformPackageInstallation(IProgress progress, string target, PackageVersionInfo package, CancellationToken cancellationToken) { + // TODO: Remove the `string target` argument as it is not used any longer + var extension = Path.GetExtension(package.targetFilename); var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase); From f5f7de64de2a15164ef6b62cb8da844dd823067d Mon Sep 17 00:00:00 2001 From: John Taylor Date: Sat, 6 Apr 2019 13:40:19 -0400 Subject: [PATCH 0312/6208] Use TLS 1.2 to download NSSM --- deployment/windows/build-jellyfin.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index 2c83f264c2..2999912b3a 100644 --- a/deployment/windows/build-jellyfin.ps1 +++ b/deployment/windows/build-jellyfin.ps1 @@ -26,7 +26,10 @@ function Build-JellyFin { Write-Error "arm only supported with Windows 8 or higher" exit } - dotnet publish -c $BuildType -r "$windowsversion-$Architecture" MediaBrowser.sln -o $InstallLocation -v $DotNetVerbosity + Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" + Write-Verbose "InstallLocation: $InstallLocation" + Write-Verbose "DotNetVerbosity: $DotNetVerbosity" + dotnet publish -c $BuildType -r `"$windowsversion-$Architecture`" MediaBrowser.sln -o $InstallLocation -v $DotNetVerbosity } function Install-FFMPEG { @@ -73,6 +76,7 @@ function Install-NSSM { Write-Warning "NSSM will not be installed" }else{ Write-Verbose "Downloading NSSM" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose } From 1af9c047fbc0283f7abfb4b98918454258dfb348 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 7 Apr 2019 19:51:45 -0400 Subject: [PATCH 0313/6208] Override username with AuthenticationProvider Pass back the Username directive returned by an AuthenticationProvider to the calling code, so we may override the user-provided Username value if the authentication provider passes this back. Useful for instance in an LDAP scenario where what the user types may not necessarily be the "username" that is mapped in the system, e.g. the user providing 'mail' while 'uid' is the "username" value. Could also then be extensible to other authentication providers as well, should they wish to do a similar thing. --- .../Library/UserManager.cs | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 75c82ca715..952cc6896b 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -277,24 +277,35 @@ namespace Emby.Server.Implementations.Library .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.Item1; - success = authResult.Item2; + updatedUsername = authResult.Item2; + success = authResult.Item3; } else { // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.Item1; - success = authResult.Item2; + updatedUsername = authResult.Item2; + success = authResult.Item3; if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) { - user = await CreateUser(username).ConfigureAwait(false); + // We should trust the user that the authprovider says, not what was typed + if (updatedUsername != username) + { + username = updatedUsername; + } + + // Search the database for the user again; the authprovider might have created it + user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; if (hasNewUserPolicy != null) @@ -414,32 +425,40 @@ namespace Emby.Server.Implementations.Library return providers; } - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + private async Task> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try { var requiresResolvedUser = provider as IRequiresResolvedUser; + ProviderAuthenticationResult authenticationResult = null; if (requiresResolvedUser != null) { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); } else { - await provider.Authenticate(username, password).ConfigureAwait(false); + authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); } - return true; + if(authenticationResult.Username != username) + { + _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); + username = authenticationResult.Username; + } + + return new Tuple(username, true); } catch (Exception ex) { _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - return false; + return new Tuple(username, false); } } - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { + string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; @@ -458,11 +477,14 @@ namespace Emby.Server.Implementations.Library { foreach (var provider in GetAuthenticationProviders(user)) { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + updatedUsername = providerAuthResult.Item1; + success = providerAuthResult.Item2; if (success) { authenticationProvider = provider; + username = updatedUsername; break; } } @@ -484,7 +506,7 @@ namespace Emby.Server.Implementations.Library } } - return new Tuple(authenticationProvider, success); + return new Tuple(authenticationProvider, username, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) From f96d1e9e690f2f0e83d153e4ae8c2f4d810e7e2e Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Sun, 7 Apr 2019 22:26:27 -0400 Subject: [PATCH 0314/6208] Fix README documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f635bdd25..6a206eacf6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ For more information about the project, please see our [about page](https://jell

Want to get started? -Choose from Prebuilt Packages or Build from Source, then see our first-time setup guide. +Choose from Prebuilt Packages or Build from Source, then see our quick start guide.

Want to contribute? From c72393c9704d693b3beee51ebd35da0f68d13261 Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Mon, 8 Apr 2019 14:56:42 +0930 Subject: [PATCH 0315/6208] Updated Unraid Docker icon Logo was set to use emby, but binhex has since added the jellyfin logo. --- deployment/unraid/docker-templates/jellyfin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index 1d97a9f00f..5a5b4bb5c7 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -46,6 +46,6 @@ http://[IP]:[PORT:8096]/ - https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/emby-icon.png + https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/jellyfin-icon.png From 7b4e16bb8ffaa95619a5be0f231e163da6747665 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 9 Apr 2019 00:43:25 +0200 Subject: [PATCH 0316/6208] Disabled dotnet_compat part of pipeline. --- .ci/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 7a92d40889..b3389caba9 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -99,7 +99,7 @@ jobs: pool: vmImage: ubuntu-16.04 dependsOn: main_build - condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) + condition: false #and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) strategy: matrix: Naming: From a7e31ef31f0202cefe1fd97a648260c3f4791446 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Thu, 4 Apr 2019 23:04:54 -0700 Subject: [PATCH 0317/6208] applied changes to just also search jellyfin base dir --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4867c0f859..b626600fa4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -230,6 +230,11 @@ namespace MediaBrowser.MediaEncoding.Encoder /// private string ExistsOnSystemPath(string filename) { + string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename); + if (!string.IsNullOrEmpty(inJellyfinPath)) + { + return inJellyfinPath; + } var values = Environment.GetEnvironmentVariable("PATH"); foreach (var path in values.Split(Path.PathSeparator)) From a1d50a6d055c414c95fa0ea2bf58db79afb6dc74 Mon Sep 17 00:00:00 2001 From: Ulysse <5031221+voodoos@users.noreply.github.com> Date: Tue, 9 Apr 2019 20:19:27 +0200 Subject: [PATCH 0318/6208] Clean `WebSocketSharpRequest.PathInfo` (#1212) * rm useless ResolvePathInfoFromMappedPath method * rm useless NormalizePathInfo method * Use request.Path instead of RawUrl * Removing unused `HandlerFactoryPath` field * Use an expression body definition and rm field `pathInfo` * More (syntactic) sugar * Who needs blocks in cases ? --- .../SocketSharp/WebSocketSharpRequest.cs | 114 +----------------- 1 file changed, 1 insertion(+), 113 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index e0a0ee2861..792615a0f6 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.SocketSharp this.OperationName = operationName; this.request = httpContext; this.Response = new WebSocketSharpResponse(logger, response); - - // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); } public HttpRequest HttpRequest => request; @@ -100,7 +98,6 @@ namespace Emby.Server.Implementations.SocketSharp switch (crlf) { case 0: - { if (c == '\r') { crlf = 1; @@ -117,10 +114,8 @@ namespace Emby.Server.Implementations.SocketSharp } break; - } case 1: - { if (c == '\n') { crlf = 2; @@ -128,10 +123,8 @@ namespace Emby.Server.Implementations.SocketSharp } throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } case 2: - { if (c == ' ' || c == '\t') { crlf = 0; @@ -139,7 +132,6 @@ namespace Emby.Server.Implementations.SocketSharp } throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } } } @@ -312,97 +304,7 @@ namespace Emby.Server.Implementations.SocketSharp return pos == -1 ? strVal : strVal.Slice(0, pos); } - public static string HandlerFactoryPath; - - private string pathInfo; - public string PathInfo - { - get - { - if (this.pathInfo == null) - { - var mode = HandlerFactoryPath; - - var pos = RawUrl.IndexOf("?", StringComparison.Ordinal); - if (pos != -1) - { - var path = RawUrl.Substring(0, pos); - this.pathInfo = GetPathInfo( - path, - mode, - mode ?? string.Empty); - } - else - { - this.pathInfo = RawUrl; - } - - this.pathInfo = WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString(); - } - - return this.pathInfo; - } - } - - private static string GetPathInfo(string fullPath, string mode, string appPath) - { - var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - // Wildcard mode relies on this to work out the handlerPath - pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - return fullPath; - } - - private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) - { - if (mappedPathRoot == null) - { - return null; - } - - var sbPathInfo = new StringBuilder(); - var fullPathParts = fullPath.Split('/'); - var mappedPathRootParts = mappedPathRoot.Split('/'); - var fullPathIndexOffset = mappedPathRootParts.Length - 1; - var pathRootFound = false; - - for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) - { - if (pathRootFound) - { - sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); - } - else if (fullPathIndex - fullPathIndexOffset >= 0) - { - pathRootFound = true; - for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) - { - if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) - { - pathRootFound = false; - break; - } - } - } - } - - if (!pathRootFound) - { - return null; - } - - return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/"; - } + public string PathInfo => this.request.Path.Value; public string UserAgent => request.Headers[HeaderNames.UserAgent]; @@ -500,19 +402,5 @@ namespace Emby.Server.Implementations.SocketSharp return httpFiles; } } - - public static ReadOnlySpan NormalizePathInfo(string pathInfo, string handlerPath) - { - if (handlerPath != null) - { - var trimmed = pathInfo.AsSpan().TrimStart('/'); - if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return trimmed.Slice(handlerPath.Length).ToString().AsSpan(); - } - } - - return pathInfo.AsSpan(); - } } } From efb14f0b583d570cf73807715ee50956badeaf1d Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 10 Apr 2019 00:23:39 -0400 Subject: [PATCH 0319/6208] Bump dockerfile web versions too --- bump_version | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/bump_version b/bump_version index b118af54b9..398caee15c 100755 --- a/bump_version +++ b/bump_version @@ -54,6 +54,7 @@ old_version="$( grep "AssemblyVersion" ${shared_version_file} \ | sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/' )" +echo $old_version # Set the shared version to the specified new_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars @@ -62,9 +63,11 @@ sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file} old_version="$( grep "version:" ${build_file} \ - | sed -E 's/version: "([0-9\.]+)"/\1/' + | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/' )" +echo $old_version +# Set the build.yaml version to the specified new_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars sed -i "s/${old_version_sed}/${new_version}/g" ${build_file} @@ -74,6 +77,16 @@ else new_version_deb="${new_version}-1" fi +# Set the Dockerfile web version to the specified new_version +old_version="$( + grep "JELLYFIN_WEB_VERSION=" Dockerfile \ + | sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/' +)" +echo $old_version + +old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars +sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile* + # Write out a temporary Debian changelog with our new stuff appended and some templated formatting debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog" debian_changelog_temp="$( mktemp )" @@ -124,5 +137,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file} rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir} # Stage the changed files for commit -git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} +git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile* git status From 65bff1181a3ae739dedbfd647d890630aa2cfba8 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 10 Apr 2019 00:51:21 -0400 Subject: [PATCH 0320/6208] Bump version to 10.3.0-rc2 and update submodule --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- MediaBrowser.WebDashboard/jellyfin-web | 2 +- build.yaml | 2 +- deployment/debian-package-x64/pkg-src/changelog | 6 ++++++ deployment/fedora-package-x64/pkg-src/jellyfin.spec | 2 ++ 7 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index dbbed535f8..36fc43983e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update \ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm b/Dockerfile.arm index 6bd5b576f1..c896d83f27 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -30,7 +30,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 3be2fd4f14..09c9dc12f8 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -31,7 +31,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc1 +ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index 9677981344..e07edea5bf 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit 9677981344e57e8f84ca664cad13da1f89f4254f +Subproject commit e07edea5bf22c253fc7ee91f45879d8ee2d1bf17 diff --git a/build.yaml b/build.yaml index 34d356dacf..ccd7ce8f85 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.3.0-rc1" +version: "10.3.0-rc2" packages: - debian-package-x64 - debian-package-armhf diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index da9151af1a..65880620dc 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,9 @@ +jellyfin (10.3.0~rc2) unstable; urgency=medium + + * New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 + + -- Jellyfin Packaging Team Wed, 10 Apr 2019 00:51:14 -0400 + jellyfin (10.3.0~rc1) unstable; urgency=medium * New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 77c291c872..581d926fbf 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -140,6 +140,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Wed Apr 10 2019 Jellyfin Packaging Team +- New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 * Sat Mar 30 2019 Jellyfin Packaging Team - New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 * Thu Feb 28 2019 Jellyfin Packaging Team From f888c4b641218557265148c318074a227a79df9e Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Thu, 11 Apr 2019 03:19:05 +0930 Subject: [PATCH 0321/6208] Fix missing Unraid cache mount Cache folder was not mounted outside of the Docker image since its separation from the config folder. Config HostDir was only updated for consistency, previous directory was overridden by unraid into the appdata/appname folder anyway. Name capitalization was corrected as this is only used by new installations & does not affect current installations/updates. --- deployment/unraid/docker-templates/jellyfin.xml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index 5a5b4bb5c7..eec9967bbe 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -3,14 +3,15 @@ https://raw.githubusercontent.com/jellyfin/jellyfin/deployment/unraid/docker-templates/jellyfin.xml False MediaApp:Video MediaApp:Music MediaApp:Photos MediaServer:Video MediaServer:Music MediaServer:Photos - JellyFin + Jellyfin - JellyFin is The Free Software Media Browser Converted By Community Applications Always verify this template (and values) against the dockerhub support page for the container!![br][br] + Jellyfin is The Free Software Media Browser Converted By Community Applications Always verify this template (and values) against the dockerhub support page for the container!![br][br] You can add as many mount points as needed for recordings, movies ,etc. [br][br] [b][span style='color: #E80000;']Directions:[/span][/b][br] - [b]/config[/b] : this is where Jellyfin will store it's databases and configuration.[br][br] + [b]/config[/b] : This is where Jellyfin will store it's databases and configuration.[br][br] [b]Port[/b] : This is the default port for Jellyfin. (Will add ssl port later)[br][br] - [b]Media[/b] : This is the mounting point of your media. When you access it in Jellyfin it will be /media or whatever you chose for a mount point + [b]Media[/b] : This is the mounting point of your media. When you access it in Jellyfin it will be /media or whatever you chose for a mount point[br][br] + [b]Cache[/b] : This is where Jellyfin will store and manage cached files like images to serve to clients. This is not where all images are stored.[br][br] [b]Tip:[/b] You can add more volume mappings if you wish Jellyfin has access to it. @@ -35,7 +36,7 @@ - /mnt/cache/appdata/config + /mnt/user/appdata/jellyfin /config rw @@ -44,6 +45,11 @@ /media rw + + /mnt/user/appdata/jellyfin/cache/ + /cache + rw + http://[IP]:[PORT:8096]/ https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/jellyfin-icon.png From a9f790e101804a2ce0a85217c5c5874e0a7cdb91 Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Thu, 11 Apr 2019 04:00:46 +0930 Subject: [PATCH 0322/6208] Fix directory capitalization --- deployment/unraid/docker-templates/jellyfin.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index eec9967bbe..57b4cc5ae1 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -36,7 +36,7 @@ - /mnt/user/appdata/jellyfin + /mnt/user/appdata/Jellyfin /config rw @@ -46,7 +46,7 @@ rw - /mnt/user/appdata/jellyfin/cache/ + /mnt/user/appdata/Jellyfin/cache/ /cache rw From 5f6ab836dea4341844f1e1e5a1e1d45c7c6621c1 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 15:35:06 +0200 Subject: [PATCH 0323/6208] Extend Microsoft.Net.Http.Headers.HeaderNames --- MediaBrowser.Common/Net/MoreHeaderNames.cs | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 MediaBrowser.Common/Net/MoreHeaderNames.cs diff --git a/MediaBrowser.Common/Net/MoreHeaderNames.cs b/MediaBrowser.Common/Net/MoreHeaderNames.cs new file mode 100644 index 0000000000..669646db1e --- /dev/null +++ b/MediaBrowser.Common/Net/MoreHeaderNames.cs @@ -0,0 +1,83 @@ +using HN = Microsoft.Net.Http.Headers.HeaderNames; + +namespace MediaBrowser.Common.Net +{ + public static class MoreHeaderNames + { + // Other Headers + public const string XForwardedFor = "X-Forwarded-For"; + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + public const string XRealIP = "X-Real-IP"; + + // Headers from Microsoft.Net.Http.Headers.HeaderNames + public const string Accept = HN.Accept; + public const string AcceptCharset = HN.AcceptCharset; + public const string AcceptEncoding = HN.AcceptEncoding; + public const string AcceptLanguage = HN.AcceptLanguage; + public const string AcceptRanges = HN.AcceptRanges; + public const string AccessControlAllowCredentials = HN.AccessControlAllowCredentials; + public const string AccessControlAllowHeaders = HN.AccessControlAllowHeaders; + public const string AccessControlAllowMethods = HN.AccessControlAllowMethods; + public const string AccessControlAllowOrigin = HN.AccessControlAllowOrigin; + public const string AccessControlExposeHeaders = HN.AccessControlExposeHeaders; + public const string AccessControlMaxAge = HN.AccessControlMaxAge; + public const string AccessControlRequestHeaders = HN.AccessControlRequestHeaders; + public const string AccessControlRequestMethod = HN.AccessControlRequestMethod; + public const string Age = HN.Age; + public const string Allow = HN.Allow; + public const string Authority = HN.Authority; + public const string Authorization = HN.Authorization; + public const string CacheControl = HN.CacheControl; + public const string Connection = HN.Connection; + public const string ContentDisposition = HN.ContentDisposition; + public const string ContentEncoding = HN.ContentEncoding; + public const string ContentLanguage = HN.ContentLanguage; + public const string ContentLength = HN.ContentLength; + public const string ContentLocation = HN.ContentLocation; + public const string ContentMD5 = HN.ContentMD5; + public const string ContentRange = HN.ContentRange; + public const string ContentSecurityPolicy = HN.ContentSecurityPolicy; + public const string ContentSecurityPolicyReportOnly = HN.ContentSecurityPolicyReportOnly; + public const string ContentType = HN.ContentType; + public const string Cookie = HN.Cookie; + public const string Date = HN.Date; + public const string ETag = HN.ETag; + public const string Expires = HN.Expires; + public const string Expect = HN.Expect; + public const string From = HN.From; + public const string Host = HN.Host; + public const string IfMatch = HN.IfMatch; + public const string IfModifiedSince = HN.IfModifiedSince; + public const string IfNoneMatch = HN.IfNoneMatch; + public const string IfRange = HN.IfRange; + public const string IfUnmodifiedSince = HN.IfUnmodifiedSince; + public const string LastModified = HN.LastModified; + public const string Location = HN.Location; + public const string MaxForwards = HN.MaxForwards; + public const string Method = HN.Method; + public const string Origin = HN.Origin; + public const string Path = HN.Path; + public const string Pragma = HN.Pragma; + public const string ProxyAuthenticate = HN.ProxyAuthenticate; + public const string ProxyAuthorization = HN.ProxyAuthorization; + public const string Range = HN.Range; + public const string Referer = HN.Referer; + public const string RetryAfter = HN.RetryAfter; + public const string Scheme = HN.Scheme; + public const string Server = HN.Server; + public const string SetCookie = HN.SetCookie; + public const string Status = HN.Status; + public const string StrictTransportSecurity = HN.StrictTransportSecurity; + public const string TE = HN.TE; + public const string Trailer = HN.Trailer; + public const string TransferEncoding = HN.TransferEncoding; + public const string Upgrade = HN.Upgrade; + public const string UserAgent = HN.UserAgent; + public const string Vary = HN.Vary; + public const string Via = HN.Via; + public const string Warning = HN.Warning; + public const string WebSocketSubProtocols = HN.WebSocketSubProtocols; + public const string WWWAuthenticate = HN.WWWAuthenticate; + } +} \ No newline at end of file From a6e1b23eb04d620e241e4babbcb6ee0ffbf156a9 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 16:58:28 +0200 Subject: [PATCH 0324/6208] Simplify headers use in WSS --- .../SocketSharp/WebSocketSharpRequest.cs | 16 +++++---------- MediaBrowser.Model/Services/IHttpRequest.cs | 20 ------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 792615a0f6..957371df62 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using HeaderNames = MediaBrowser.Common.Net.MoreHeaderNames; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IResponse = MediaBrowser.Model.Services.IResponse; @@ -38,16 +39,9 @@ namespace Emby.Server.Implementations.SocketSharp public string RawUrl => request.GetEncodedPathAndQuery(); public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); + // Header[name] returns "" when undefined - public string XForwardedFor - => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); - - public int? XForwardedPort - => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); - - public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString(); - - public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString(); + private string GetHeader(string name) => request.Headers[name].ToString(); private string remoteIp; public string RemoteIp @@ -59,13 +53,13 @@ namespace Emby.Server.Implementations.SocketSharp return remoteIp; } - var temp = CheckBadChars(XForwardedFor.AsSpan()); + var temp = CheckBadChars(GetHeader(HeaderNames.XForwardedFor).AsSpan()); if (temp.Length != 0) { return remoteIp = temp.ToString(); } - temp = CheckBadChars(XRealIp.AsSpan()); + temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); if (temp.Length != 0) { return remoteIp = NormalizeIp(temp).ToString(); diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs index 50c6076f30..daf91488f0 100644 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ b/MediaBrowser.Model/Services/IHttpRequest.cs @@ -7,26 +7,6 @@ namespace MediaBrowser.Model.Services ///

string HttpMethod { get; } - /// - /// The IP Address of the X-Forwarded-For header, null if null or empty - /// - string XForwardedFor { get; } - - /// - /// The Port number of the X-Forwarded-Port header, null if null or empty - /// - int? XForwardedPort { get; } - - /// - /// The http or https scheme of the X-Forwarded-Proto header, null if null or empty - /// - string XForwardedProtocol { get; } - - /// - /// The value of the X-Real-IP header, null if null or empty - /// - string XRealIp { get; } - /// /// The value of the Accept HTTP Request Header /// From 56d1050bac3a56249acd7f3b3615f796683e0783 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 17:13:40 +0200 Subject: [PATCH 0325/6208] Replace custom ip "normalization" by methods from `IPAddress` --- .../SocketSharp/WebSocketSharpRequest.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 957371df62..d153a85a32 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -62,10 +62,10 @@ namespace Emby.Server.Implementations.SocketSharp temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); if (temp.Length != 0) { - return remoteIp = NormalizeIp(temp).ToString(); + return remoteIp = NormalizeIp(temp.ToString()).ToString(); } - return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString(); + return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress).ToString(); } } @@ -137,22 +137,21 @@ namespace Emby.Server.Implementations.SocketSharp return name; } - private ReadOnlySpan NormalizeIp(ReadOnlySpan ip) + private IPAddress NormalizeIp(IPAddress ip) { - if (ip.Length != 0 && !ip.IsWhiteSpace()) + if (ip.IsIPv4MappedToIPv6) { - // Handle ipv4 mapped to ipv6 - const string srch = "::ffff:"; - var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase); - if (index == 0) - { - ip = ip.Slice(srch.Length); - } + return ip.MapToIPv4(); } return ip; } + private IPAddress NormalizeIp(string sip) + { + return NormalizeIp(IPAddress.Parse(sip)); + } + public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; From bb807554e2c33f066402898a3ff79913865d50e3 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 17:17:48 +0200 Subject: [PATCH 0326/6208] Replace CRLF injection mitigation by use of .NET ip parsing --- .../SocketSharp/WebSocketSharpRequest.cs | 95 +++---------------- 1 file changed, 11 insertions(+), 84 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index d153a85a32..38a860a511 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -53,91 +53,23 @@ namespace Emby.Server.Implementations.SocketSharp return remoteIp; } - var temp = CheckBadChars(GetHeader(HeaderNames.XForwardedFor).AsSpan()); - if (temp.Length != 0) + IPAddress ip; + + // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip + // (if the server is behind a reverse proxy for example) + if (!IPAddress.TryParse(GetHeader(HeaderNames.XForwardedFor), out ip)) { - return remoteIp = temp.ToString(); + if (!IPAddress.TryParse(GetHeader(HeaderNames.XRealIP), out ip)) + { + ip = request.HttpContext.Connection.RemoteIpAddress; + } } - temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); - if (temp.Length != 0) - { - return remoteIp = NormalizeIp(temp.ToString()).ToString(); - } - - return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress).ToString(); + return remoteIp = NormalizeIp(ip).ToString(); } } - private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; - - // CheckBadChars - throws on invalid chars to be not found in header name/value - internal static ReadOnlySpan CheckBadChars(ReadOnlySpan name) - { - if (name.Length == 0) - { - return name; - } - - // VALUE check - // Trim spaces from both ends - name = name.Trim(HttpTrimCharacters); - - // First, check for correctly formed multi-line value - // Second, check for absence of CTL characters - int crlf = 0; - for (int i = 0; i < name.Length; ++i) - { - char c = (char)(0x000000ff & (uint)name[i]); - switch (crlf) - { - case 0: - if (c == '\r') - { - crlf = 1; - } - else if (c == '\n') - { - // Technically this is bad HTTP. But it would be a breaking change to throw here. - // Is there an exploit? - crlf = 2; - } - else if (c == 127 || (c < ' ' && c != '\t')) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name)); - } - - break; - - case 1: - if (c == '\n') - { - crlf = 2; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - - case 2: - if (c == ' ' || c == '\t') - { - crlf = 0; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } - } - - if (crlf != 0) - { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } - - return name; - } - - private IPAddress NormalizeIp(IPAddress ip) + private static IPAddress NormalizeIp(IPAddress ip) { if (ip.IsIPv4MappedToIPv6) { @@ -147,11 +79,6 @@ namespace Emby.Server.Implementations.SocketSharp return ip; } - private IPAddress NormalizeIp(string sip) - { - return NormalizeIp(IPAddress.Parse(sip)); - } - public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; From ba12d96d23a53ce16a1da1b2fcf68a301050b858 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Fri, 12 Apr 2019 15:25:18 +0200 Subject: [PATCH 0327/6208] Removed wrapping of HeaderNames fields --- .../SocketSharp/WebSocketSharpRequest.cs | 6 +- MediaBrowser.Common/Net/CustomHeaderNames.cs | 11 +++ MediaBrowser.Common/Net/MoreHeaderNames.cs | 83 ------------------- 3 files changed, 14 insertions(+), 86 deletions(-) create mode 100644 MediaBrowser.Common/Net/CustomHeaderNames.cs delete mode 100644 MediaBrowser.Common/Net/MoreHeaderNames.cs diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 38a860a511..00465b63eb 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -4,13 +4,13 @@ using System.Globalization; using System.IO; using System.Net; using System.Text; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using HeaderNames = MediaBrowser.Common.Net.MoreHeaderNames; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IResponse = MediaBrowser.Model.Services.IResponse; @@ -57,9 +57,9 @@ namespace Emby.Server.Implementations.SocketSharp // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(HeaderNames.XForwardedFor), out ip)) + if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) { - if (!IPAddress.TryParse(GetHeader(HeaderNames.XRealIP), out ip)) + if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) { ip = request.HttpContext.Connection.RemoteIpAddress; } diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs new file mode 100644 index 0000000000..ff148dc801 --- /dev/null +++ b/MediaBrowser.Common/Net/CustomHeaderNames.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Common.Net +{ + public static class CustomHeaderNames + { + // Other Headers + public const string XForwardedFor = "X-Forwarded-For"; + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + public const string XRealIP = "X-Real-IP"; + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Net/MoreHeaderNames.cs b/MediaBrowser.Common/Net/MoreHeaderNames.cs deleted file mode 100644 index 669646db1e..0000000000 --- a/MediaBrowser.Common/Net/MoreHeaderNames.cs +++ /dev/null @@ -1,83 +0,0 @@ -using HN = Microsoft.Net.Http.Headers.HeaderNames; - -namespace MediaBrowser.Common.Net -{ - public static class MoreHeaderNames - { - // Other Headers - public const string XForwardedFor = "X-Forwarded-For"; - public const string XForwardedPort = "X-Forwarded-Port"; - public const string XForwardedProto = "X-Forwarded-Proto"; - public const string XRealIP = "X-Real-IP"; - - // Headers from Microsoft.Net.Http.Headers.HeaderNames - public const string Accept = HN.Accept; - public const string AcceptCharset = HN.AcceptCharset; - public const string AcceptEncoding = HN.AcceptEncoding; - public const string AcceptLanguage = HN.AcceptLanguage; - public const string AcceptRanges = HN.AcceptRanges; - public const string AccessControlAllowCredentials = HN.AccessControlAllowCredentials; - public const string AccessControlAllowHeaders = HN.AccessControlAllowHeaders; - public const string AccessControlAllowMethods = HN.AccessControlAllowMethods; - public const string AccessControlAllowOrigin = HN.AccessControlAllowOrigin; - public const string AccessControlExposeHeaders = HN.AccessControlExposeHeaders; - public const string AccessControlMaxAge = HN.AccessControlMaxAge; - public const string AccessControlRequestHeaders = HN.AccessControlRequestHeaders; - public const string AccessControlRequestMethod = HN.AccessControlRequestMethod; - public const string Age = HN.Age; - public const string Allow = HN.Allow; - public const string Authority = HN.Authority; - public const string Authorization = HN.Authorization; - public const string CacheControl = HN.CacheControl; - public const string Connection = HN.Connection; - public const string ContentDisposition = HN.ContentDisposition; - public const string ContentEncoding = HN.ContentEncoding; - public const string ContentLanguage = HN.ContentLanguage; - public const string ContentLength = HN.ContentLength; - public const string ContentLocation = HN.ContentLocation; - public const string ContentMD5 = HN.ContentMD5; - public const string ContentRange = HN.ContentRange; - public const string ContentSecurityPolicy = HN.ContentSecurityPolicy; - public const string ContentSecurityPolicyReportOnly = HN.ContentSecurityPolicyReportOnly; - public const string ContentType = HN.ContentType; - public const string Cookie = HN.Cookie; - public const string Date = HN.Date; - public const string ETag = HN.ETag; - public const string Expires = HN.Expires; - public const string Expect = HN.Expect; - public const string From = HN.From; - public const string Host = HN.Host; - public const string IfMatch = HN.IfMatch; - public const string IfModifiedSince = HN.IfModifiedSince; - public const string IfNoneMatch = HN.IfNoneMatch; - public const string IfRange = HN.IfRange; - public const string IfUnmodifiedSince = HN.IfUnmodifiedSince; - public const string LastModified = HN.LastModified; - public const string Location = HN.Location; - public const string MaxForwards = HN.MaxForwards; - public const string Method = HN.Method; - public const string Origin = HN.Origin; - public const string Path = HN.Path; - public const string Pragma = HN.Pragma; - public const string ProxyAuthenticate = HN.ProxyAuthenticate; - public const string ProxyAuthorization = HN.ProxyAuthorization; - public const string Range = HN.Range; - public const string Referer = HN.Referer; - public const string RetryAfter = HN.RetryAfter; - public const string Scheme = HN.Scheme; - public const string Server = HN.Server; - public const string SetCookie = HN.SetCookie; - public const string Status = HN.Status; - public const string StrictTransportSecurity = HN.StrictTransportSecurity; - public const string TE = HN.TE; - public const string Trailer = HN.Trailer; - public const string TransferEncoding = HN.TransferEncoding; - public const string Upgrade = HN.Upgrade; - public const string UserAgent = HN.UserAgent; - public const string Vary = HN.Vary; - public const string Via = HN.Via; - public const string Warning = HN.Warning; - public const string WebSocketSubProtocols = HN.WebSocketSubProtocols; - public const string WWWAuthenticate = HN.WWWAuthenticate; - } -} \ No newline at end of file From b2f94c0e4015160ba3102cd6617397fc07c6ba35 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Mon, 15 Apr 2019 00:21:14 -0400 Subject: [PATCH 0328/6208] Remove the old message responders Leaves only an answer to "Who is Jellyfin", removing older ones for EmbyServer and MediaBrowser_v2. --- Emby.Server.Implementations/Udp/UdpServer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index bd86c6cdc8..e7cda2993f 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -41,8 +41,6 @@ namespace Emby.Server.Implementations.Udp _socketFactory = socketFactory; AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message); - AddMessageResponder("who is EmbyServer?", true, RespondToV2Message); - AddMessageResponder("who is MediaBrowserServer_v2?", false, RespondToV2Message); } private void AddMessageResponder(string message, bool isSubstring, Func responder) From 65f9141764c18b76d97925e531816e8dd383f4bb Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Wed, 10 Apr 2019 22:08:23 -0400 Subject: [PATCH 0329/6208] Update the help message to indicate the right output folder --- build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build b/build index 51d4b79a20..fb4ff1984a 100755 --- a/build +++ b/build @@ -29,7 +29,7 @@ usage() { echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching." echo -e "To build all platforms, use 'all'." echo -e "To perform all build actions, use 'all'." - echo -e "Build output files are collected at '../jellyfin-build/'." + echo -e "Build output files are collected at '../bin/'." } # Show usage on stderr with exit 1 on argless From 34ab99caf18ee862a1dc29a6afb83253db35c219 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Tue, 16 Apr 2019 01:16:02 -0400 Subject: [PATCH 0330/6208] Move the ProductName to the public endpoint Moves the ProductName field over from the private system/info point to the public one, for easier identification --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 05f8a8a5e7..82042f5ca7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1425,7 +1425,6 @@ namespace Emby.Server.Implementations HasPendingRestart = HasPendingRestart, IsShuttingDown = IsShuttingDown, Version = ApplicationVersion, - ProductName = ApplicationProductName, WebSocketPortNumber = HttpPort, CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), Id = SystemId, @@ -1482,6 +1481,7 @@ namespace Emby.Server.Implementations return new PublicSystemInfo { Version = ApplicationVersion, + ProductName = ApplicationProductName, Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), WanAddress = wanAddress, From 2f0719a883580b547960904703a39748281bf5e0 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Tue, 16 Apr 2019 01:38:00 -0400 Subject: [PATCH 0331/6208] Move the definition of ProductName to the correct class Missed moving this from one class to the other. --- MediaBrowser.Model/System/PublicSystemInfo.cs | 5 +++++ MediaBrowser.Model/System/SystemInfo.cs | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index d97eda3523..d6e031e427 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -25,6 +25,11 @@ namespace MediaBrowser.Model.System ///
/// The version. public string Version { get; set; } + + /// + /// The product name. This is the AssemblyProduct name. + /// + public string ProductName { get; set; } /// /// Gets or sets the operating system. diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 222c10798a..3f73cc4e0b 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -32,10 +32,6 @@ namespace MediaBrowser.Model.System /// The display name of the operating system. public string OperatingSystemDisplayName { get; set; } - /// - /// The product name. This is the AssemblyProduct name. - /// - public string ProductName { get; set; } /// /// Get or sets the package name. From c7fedfbca388d3e43ca266e7a4a6caccfc4bb5c7 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 17 Apr 2019 11:41:14 +0200 Subject: [PATCH 0332/6208] Fix metadata path save --- .../Configuration/ServerConfigurationManager.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 18e279c2f0..c4fa68cac4 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration /// private void UpdateMetadataPath() { - string metadataPath; - if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) { - metadataPath = GetInternalMetadataPath(); + ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata"); } else { - metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); + ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath; } - - ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath; - } - - private string GetInternalMetadataPath() - { - return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata"); } /// From e89c8dbf7623fcb0c19c96957f6260df31fc3945 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 17 Apr 2019 15:23:03 +0200 Subject: [PATCH 0333/6208] Use CultureInfo.InvariantCulture --- .../Playback/BaseStreamingService.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index efbb17fd2a..3994016240 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -30,8 +30,6 @@ namespace MediaBrowser.Api.Playback /// public abstract class BaseStreamingService : BaseApiService { - protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - protected virtual bool EnableOutputInSubFolder => false; /// @@ -418,55 +416,55 @@ namespace MediaBrowser.Api.Playback { if (videoRequest != null) { - videoRequest.AudioStreamIndex = int.Parse(val, UsCulture); + videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 7) { if (videoRequest != null) { - videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture); + videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 8) { if (videoRequest != null) { - videoRequest.VideoBitRate = int.Parse(val, UsCulture); + videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 9) { - request.AudioBitRate = int.Parse(val, UsCulture); + request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 10) { - request.MaxAudioChannels = int.Parse(val, UsCulture); + request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 11) { if (videoRequest != null) { - videoRequest.MaxFramerate = float.Parse(val, UsCulture); + videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 12) { if (videoRequest != null) { - videoRequest.MaxWidth = int.Parse(val, UsCulture); + videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 13) { if (videoRequest != null) { - videoRequest.MaxHeight = int.Parse(val, UsCulture); + videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 14) { - request.StartTimeTicks = long.Parse(val, UsCulture); + request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); } else if (i == 15) { @@ -479,14 +477,14 @@ namespace MediaBrowser.Api.Playback { if (videoRequest != null) { - videoRequest.MaxRefFrames = int.Parse(val, UsCulture); + videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 17) { if (videoRequest != null) { - videoRequest.MaxVideoBitDepth = int.Parse(val, UsCulture); + videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 18) @@ -535,7 +533,7 @@ namespace MediaBrowser.Api.Playback } else if (i == 26) { - request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); + request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 27) { @@ -640,7 +638,7 @@ namespace MediaBrowser.Api.Playback if (value.IndexOf(':') == -1) { // Parses npt times in the format of '417.33' - if (double.TryParse(value, NumberStyles.Any, UsCulture, out var seconds)) + if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { return TimeSpan.FromSeconds(seconds).Ticks; } @@ -655,7 +653,7 @@ namespace MediaBrowser.Api.Playback foreach (var time in tokens) { - if (double.TryParse(time, NumberStyles.Any, UsCulture, out var digit)) + if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit)) { secondsSum += digit * timeFactor; } @@ -1027,8 +1025,8 @@ namespace MediaBrowser.Api.Playback private void AddTimeSeekResponseHeaders(StreamState state, IDictionary responseHeaders) { - var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture); - var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(UsCulture); + var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); + var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds); responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds); From 250e0c75dfaebca54e93be6c11c70cb0d19e589a Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 17 Apr 2019 22:31:06 -0400 Subject: [PATCH 0334/6208] Add MethodNotAllowedException with code 405 --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e8d47cad52..831391cee6 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -203,6 +203,7 @@ namespace Emby.Server.Implementations.HttpServer case DirectoryNotFoundException _: case FileNotFoundException _: case ResourceNotFoundException _: return 404; + case MethodNotAllowedException _: return 405; case RemoteServiceUnavailableException _: return 502; default: return 500; } From e790f024c2da2b3104ad698abfbd74fdf273bb9f Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 17 Apr 2019 22:31:17 -0400 Subject: [PATCH 0335/6208] Return MethodNotAllowedException if Pw is not set Don't accept pre-hashed (not-plaintext) passwords as the auth provider no longer supports this due to sha1+salting the passwords in the database. --- MediaBrowser.Api/UserService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index a6849f75f5..0db62098ca 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -379,6 +379,11 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("User not found"); } + if (!request.Pw) + { + throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); + } + return Post(new AuthenticateUserByName { Username = user.Name, From ca3bb308b3b20327ee96ea914cdaf02fa51374cd Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 17 Apr 2019 22:46:26 -0400 Subject: [PATCH 0336/6208] Add the proper Class too --- .../Extensions/ResourceNotFoundException.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index f62c65fd7f..9f70ae7d89 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -26,6 +26,30 @@ namespace MediaBrowser.Common.Extensions } } + /// + /// Class MethodNotAllowedException + /// + public class MethodNotAllowedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public MethodNotAllowedException() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + public MethodNotAllowedException(string message) + : base(message) + { + + } + } + public class RemoteServiceUnavailableException : Exception { public RemoteServiceUnavailableException() From eaa1ac80133e766a1d3ab4e0f5a07bc48619cd44 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Wed, 17 Apr 2019 22:49:17 -0400 Subject: [PATCH 0337/6208] Apparently strings can't be !'d --- MediaBrowser.Api/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 0db62098ca..119c423e60 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -379,7 +379,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("User not found"); } - if (!request.Pw) + if (request.Pw == "") { throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); } From 8f703f4744b2701843e316210263c8e4cd3256bd Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 18 Apr 2019 13:19:16 +0200 Subject: [PATCH 0338/6208] Remove unused event Release builds were failing because of this unused event. --- .../Activity/ActivityLogEntryPoint.cs | 14 ------------- .../ApplicationHost.cs | 21 +++++++------------ MediaBrowser.Common/IApplicationHost.cs | 5 ----- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 98cd97c318..190e4d55c0 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -83,8 +83,6 @@ namespace Emby.Server.Implementations.Activity _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - _appHost.ApplicationUpdated += OnApplicationUpdated; - return Task.CompletedTask; } @@ -275,16 +273,6 @@ namespace Emby.Server.Implementations.Activity }); } - private void OnApplicationUpdated(object sender, GenericEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr), - Type = NotificationType.ApplicationUpdateInstalled.ToString(), - Overview = e.Argument.description - }); - } - private void OnUserPolicyUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry @@ -460,8 +448,6 @@ namespace Emby.Server.Implementations.Activity _userManager.UserLockedOut -= OnUserLockedOut; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; - - _appHost.ApplicationUpdated -= OnApplicationUpdated; } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9804f28cf3..0ebbeea57e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -154,11 +154,6 @@ namespace Emby.Server.Implementations /// public event EventHandler HasPendingRestartChanged; - /// - /// Occurs when [application updated]. - /// - public event EventHandler> ApplicationUpdated; - /// /// Gets a value indicating whether this instance has changes that require the entire application to restart. /// @@ -1392,9 +1387,9 @@ namespace Emby.Server.Implementations public async Task GetSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - - string wanAddress; - + + string wanAddress; + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); @@ -1451,10 +1446,10 @@ namespace Emby.Server.Implementations public async Task GetPublicSystemInfo(CancellationToken cancellationToken) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - + var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + string wanAddress; - + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); @@ -1570,9 +1565,9 @@ namespace Emby.Server.Implementations } return string.Format("http://{0}:{1}", host, - ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture)); + ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture)); } - + public Task> GetLocalIpAddresses(CancellationToken cancellationToken) { return GetLocalIpAddressesInternal(true, 0, cancellationToken); diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 2925a3efd9..cb7343440a 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -25,11 +25,6 @@ namespace MediaBrowser.Common /// The device identifier. string SystemId { get; } - /// - /// Occurs when [application updated]. - /// - event EventHandler> ApplicationUpdated; - /// /// Gets or sets a value indicating whether this instance has pending kernel reload. /// From 10f33b027345193f91b91600473222797ae9bef5 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 18 Apr 2019 09:31:30 -0400 Subject: [PATCH 0339/6208] Update conditional to be correct --- MediaBrowser.Api/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 119c423e60..7628a2f0fa 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -379,7 +379,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("User not found"); } - if (request.Pw == "") + if (!string.IsNullOrEmpty(request.Password) || string.IsNullOrEmpty(request.Pw)) { throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); } From 31ad366aa93e8bc07f6e120320f3abd27d9dfd49 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 18 Apr 2019 10:24:08 -0400 Subject: [PATCH 0340/6208] Implemented suggested conditional --- MediaBrowser.Api/UserService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 7628a2f0fa..497800d263 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -379,7 +379,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("User not found"); } - if (!string.IsNullOrEmpty(request.Password) || string.IsNullOrEmpty(request.Pw)) + if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw)) { throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API."); } @@ -387,7 +387,7 @@ namespace MediaBrowser.Api return Post(new AuthenticateUserByName { Username = user.Name, - Password = request.Password, + Password = null, // This should always be null Pw = request.Pw }); } From 90c04a46403ba158e11c444c7109f5a56f12eff3 Mon Sep 17 00:00:00 2001 From: tinganhsu Date: Fri, 19 Apr 2019 04:32:51 +0000 Subject: [PATCH 0341/6208] Added translation using Weblate (Chinese (Traditional)) --- Emby.Server.Implementations/Localization/Core/zh_Hant.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/zh_Hant.json diff --git a/Emby.Server.Implementations/Localization/Core/zh_Hant.json b/Emby.Server.Implementations/Localization/Core/zh_Hant.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/zh_Hant.json @@ -0,0 +1 @@ +{} From ba684d6d3a4bfc9a0dc3f9de16a3b32bb95da1a1 Mon Sep 17 00:00:00 2001 From: tinganhsu Date: Fri, 19 Apr 2019 04:33:31 +0000 Subject: [PATCH 0342/6208] Translated using Weblate (Chinese (Traditional)) Currently translated at 96.8% (91 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- .../Localization/Core/zh_Hant.json | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh_Hant.json b/Emby.Server.Implementations/Localization/Core/zh_Hant.json index 0967ef424b..effff5566e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh_Hant.json +++ b/Emby.Server.Implementations/Localization/Core/zh_Hant.json @@ -1 +1,93 @@ -{} +{ + "Albums": "專輯", + "AppDeviceValues": "應用: {0}, 裝置: {1}", + "Application": "應用程式", + "Artists": "演出者", + "AuthenticationSucceededWithUserName": "{0} 成功授權", + "Books": "圖書", + "CameraImageUploadedFrom": "{0} 已經成功上傳一張相片", + "Channels": "頻道", + "ChapterNameValue": "章節 {0}", + "Collections": "合輯", + "DeviceOfflineWithName": "{0} 已經斷線", + "DeviceOnlineWithName": "{0} 已經連線", + "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", + "Favorites": "我的最愛", + "Folders": "資料夾", + "Genres": "風格", + "HeaderAlbumArtists": "專輯演出者", + "HeaderCameraUploads": "相機上傳", + "HeaderContinueWatching": "繼續觀賞", + "HeaderFavoriteAlbums": "最愛專輯", + "HeaderFavoriteArtists": "最愛演出者", + "HeaderFavoriteEpisodes": "最愛級數", + "HeaderFavoriteShows": "最愛節目", + "HeaderFavoriteSongs": "最愛歌曲", + "HeaderLiveTV": "電視直播", + "HeaderNextUp": "接下來", + "HomeVideos": "自製影片", + "ItemAddedWithName": "{0} 已新增至媒體庫", + "ItemRemovedWithName": "{0} 已從媒體庫移除", + "LabelIpAddressValue": "IP 位置: {0}", + "LabelRunningTimeValue": "運行時間: {0}", + "Latest": "最新", + "MessageApplicationUpdated": "Jellyfin Server 已經更新", + "MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新", + "MessageServerConfigurationUpdated": "伺服器設定已經更新", + "MixedContent": "混合內容", + "Movies": "電影", + "Music": "音樂", + "MusicVideos": "音樂MV", + "NameInstallFailed": "{0} 安裝失敗", + "NameSeasonNumber": "第 {0} 季", + "NameSeasonUnknown": "未知季數", + "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。", + "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新", + "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", + "NotificationOptionAudioPlayback": "音樂開始播放", + "NotificationOptionAudioPlaybackStopped": "音樂停止播放", + "NotificationOptionCameraImageUploaded": "相機相片已上傳", + "NotificationOptionInstallationFailed": "安裝失敗", + "NotificationOptionNewLibraryContent": "已新增新內容", + "NotificationOptionPluginError": "外掛失敗", + "NotificationOptionPluginInstalled": "外掛已安裝", + "NotificationOptionPluginUninstalled": "外掛已移除", + "NotificationOptionPluginUpdateInstalled": "已更新外掛", + "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", + "NotificationOptionTaskFailed": "排程任務失敗", + "NotificationOptionUserLockedOut": "使用者已鎖定", + "NotificationOptionVideoPlayback": "影片開始播放", + "NotificationOptionVideoPlaybackStopped": "影片停止播放", + "Photos": "相片", + "Playlists": "播放清單", + "Plugin": "外掛", + "PluginInstalledWithName": "{0} 已安裝", + "PluginUninstalledWithName": "{0} 已移除", + "PluginUpdatedWithName": "{0} 已更新", + "ProviderValue": "提供商: {0}", + "ScheduledTaskFailedWithName": "{0} 已失敗", + "ScheduledTaskStartedWithName": "{0} 已開始", + "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動", + "Shows": "節目", + "Songs": "歌曲", + "StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。", + "SubtitlesDownloadedForItem": "已為 {0} 下載字幕", + "Sync": "同步", + "System": "系統", + "TvShows": "電視節目", + "User": "使用者", + "UserCreatedWithName": "使用者 {0} 已建立", + "UserDeletedWithName": "使用者 {0} 已移除", + "UserDownloadingItemWithValues": "{0} 正在下載 {1}", + "UserLockedOutWithName": "使用者 {0} 已鎖定", + "UserOfflineFromDevice": "{0} 已從 {1} 斷線", + "UserOnlineFromDevice": "{0} 已連線,來自 {1}", + "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", + "UserPolicyUpdatedWithName": "使用者條約已更新為 {0}", + "UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}", + "ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫", + "ValueSpecialEpisodeName": "特典 - {0}", + "VersionNumber": "版本 {0}" +} From d6622818dca410f15764a94c2e19cf9dee1393a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Libor=20Fil=C3=ADpek?= Date: Sat, 13 Apr 2019 12:06:54 +0000 Subject: [PATCH 0343/6208] Translated using Weblate (Czech) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- .../Localization/Core/cs.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index a8b4a44244..c2a101a4da 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -5,7 +5,7 @@ "Artists": "Umělci", "AuthenticationSucceededWithUserName": "{0} úspěšně ověřen", "Books": "Knihy", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie", "Channels": "Kanály", "ChapterNameValue": "Kapitola {0}", "Collections": "Kolekce", @@ -16,14 +16,14 @@ "Folders": "Složky", "Genres": "Žánry", "HeaderAlbumArtists": "Umělci alba", - "HeaderCameraUploads": "Camera Uploads", + "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovat ve sledování", "HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteArtists": "Oblíbení umělci", "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbené písně", - "HeaderLiveTV": "Živá TV", + "HeaderLiveTV": "Live TV", "HeaderNextUp": "Nadcházející", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domáci videa", @@ -34,17 +34,17 @@ "LabelRunningTimeValue": "Délka média: {0}", "Latest": "Nejnovější", "MessageApplicationUpdated": "Jellyfin Server byl aktualizován", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována", "MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována", "MixedContent": "Smíšený obsah", "Movies": "Filmy", "Music": "Hudba", "MusicVideos": "Hudební klipy", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "Instalace {0} selhala", "NameSeasonNumber": "Sezóna {0}", "NameSeasonUnknown": "Neznámá sezóna", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NewVersionIsAvailable": "Nová verze Jellyfin serveru je k dispozici ke stažení.", "NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace", "NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována", "NotificationOptionAudioPlayback": "Přehrávání audia zahájeno", @@ -70,12 +70,12 @@ "ProviderValue": "Poskytl: {0}", "ScheduledTaskFailedWithName": "{0} selhalo", "ScheduledTaskStartedWithName": "{0} zahájeno", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} vyžaduje restart", "Shows": "Seriály", "Songs": "Skladby", "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo", "SubtitlesDownloadedForItem": "Staženy titulky pro {0}", "Sync": "Synchronizace", "System": "Systém", @@ -88,10 +88,10 @@ "UserOfflineFromDevice": "{0} se odpojil od {1}", "UserOnlineFromDevice": "{0} se připojil z {1}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", "UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií", "ValueSpecialEpisodeName": "Speciál - {0}", "VersionNumber": "Verze {0}" } From d31f5229da7049a6cef56c745ae93168f7df24d5 Mon Sep 17 00:00:00 2001 From: Dan Johansen Date: Tue, 9 Apr 2019 07:39:06 +0000 Subject: [PATCH 0344/6208] Translated using Weblate (Danish) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- Emby.Server.Implementations/Localization/Core/da.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 9d4d740996..cb8f4576ad 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -61,8 +61,8 @@ "NotificationOptionUserLockedOut": "Bruger låst ude", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", - "Photos": "Fotos", - "Playlists": "Spillelister", + "Photos": "Fotoer", + "Playlists": "Afspilningslister", "Plugin": "Plugin", "PluginInstalledWithName": "{0} blev installeret", "PluginUninstalledWithName": "{0} blev afinstalleret", From 395d2e4917f3c790bb14a1dd43e062b036d98b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=92=CE=B1=CF=83=CE=AF=CE=BB=CE=B7=CF=82=20=CE=9C=CE=BF?= =?UTF-8?q?=CF=85=CF=81=CE=B1=CF=84=CE=AF=CE=B4=CE=B7=CF=82?= Date: Thu, 18 Apr 2019 09:05:44 +0000 Subject: [PATCH 0345/6208] Translated using Weblate (Greek) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 91ca34edc2..db7ebb0c01 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -16,7 +16,7 @@ "Folders": "Φάκελοι", "Genres": "Είδη", "HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών", - "HeaderCameraUploads": "Camera Uploads", + "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας", "HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", @@ -34,7 +34,7 @@ "LabelRunningTimeValue": "Διάρκεια: {0}", "Latest": "Πρόσφατα", "MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Ο server Jellyfin αναβαθμίστηκε σε έκδοση {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί", "MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί", "MixedContent": "Ανάμεικτο Περιεχόμενο", @@ -49,7 +49,7 @@ "NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε", "NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε", "NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα", "NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης", "NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο", "NotificationOptionPluginError": "Αποτυχία του plugin", @@ -75,7 +75,7 @@ "Songs": "Τραγούδια", "StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.", "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}", "SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}", "Sync": "Συγχρονισμός", "System": "Σύστημα", From 8e2827cc39388152ae3d86cf5ec57cc1b4b00753 Mon Sep 17 00:00:00 2001 From: WWWesten Date: Tue, 2 Apr 2019 12:41:49 +0000 Subject: [PATCH 0346/6208] Translated using Weblate (Kazakh) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kk/ --- Emby.Server.Implementations/Localization/Core/kk.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 23841f37d6..cbee711551 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -5,7 +5,7 @@ "Artists": "Oryndaýshylar", "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy", "Books": "Kitaptar", - "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy", + "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy", "Channels": "Arnalar", "ChapterNameValue": "{0}-sahna", "Collections": "Jıyntyqtar", @@ -35,8 +35,8 @@ "Latest": "Eń keıingi", "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy", "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy", - "MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy", - "MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy", + "MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy", + "MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy", "MixedContent": "Aralas mazmun", "Movies": "Fılmder", "Music": "Mýzyka", @@ -49,7 +49,7 @@ "NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy", "NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy", "NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy", - "NotificationOptionCameraImageUploaded": "Kameradan fotosýret keri qotarylǵan", + "NotificationOptionCameraImageUploaded": "Kameradan fotosýret júktep salynǵan", "NotificationOptionInstallationFailed": "Ornatý sátsizdigi", "NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen", "NotificationOptionPluginError": "Plagın sátsizdigi", From 4886fc467c8cd07fbee2cfc43a2f2c6c71216da1 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Tue, 16 Apr 2019 14:53:26 +0000 Subject: [PATCH 0347/6208] Translated using Weblate (Slovenian) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/ --- .../Localization/Core/sl-SI.json | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index b50ff4706e..531dfe51f0 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -9,7 +9,7 @@ "Channels": "Kanali", "ChapterNameValue": "Poglavje {0}", "Collections": "Zbirke", - "DeviceOfflineWithName": "{0} has disconnected", + "DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOnlineWithName": "{0} je povezan", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "Favorites": "Priljubljeni", @@ -33,9 +33,9 @@ "LabelIpAddressValue": "IP naslov: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}", "Latest": "Najnovejše", - "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", - "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", + "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen", + "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", "MixedContent": "Razne vsebine", "Movies": "Filmi", @@ -57,41 +57,41 @@ "NotificationOptionPluginUninstalled": "Dodatek odstranjen", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "NotificationOptionTaskFailed": "Razporejena naloga neuspešna", + "NotificationOptionUserLockedOut": "Uporabnik zaklenjen", + "NotificationOptionVideoPlayback": "Predvajanje videa se je začelo", + "NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo", + "Photos": "Fotografije", + "Playlists": "Seznami predvajanja", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", + "PluginInstalledWithName": "{0} je bil nameščen", + "PluginUninstalledWithName": "{0} je bil odstranjen", + "PluginUpdatedWithName": "{0} je bil posodobljen", "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ScheduledTaskFailedWithName": "{0} ni uspelo", + "ScheduledTaskStartedWithName": "{0} začeto", + "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan", "Shows": "Serije", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Songs": "Pesmi", + "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", + "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}", + "SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}", + "Sync": "Sinhroniziraj", "System": "System", - "TvShows": "TV Shows", + "TvShows": "TV serije", "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "UserCreatedWithName": "Uporabnik {0} je bil ustvarjen", + "UserDeletedWithName": "Uporabnik {0} je bil izbrisan", + "UserDownloadingItemWithValues": "{0} prenaša {1}", + "UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen", + "UserOfflineFromDevice": "{0} je prekinil povezavo z {1}", + "UserOnlineFromDevice": "{0} je aktiven iz {1}", + "UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno", + "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}", + "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", + "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}" } From 10cbdc8e8e624301c2edb1598491cd6493b1644a Mon Sep 17 00:00:00 2001 From: Heldenkrieger01 Date: Tue, 9 Apr 2019 17:16:35 +0000 Subject: [PATCH 0348/6208] Translated using Weblate (Swiss German) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/ --- .../Localization/Core/gsw.json | 166 +++++++++--------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 728002a560..69c1574014 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -1,97 +1,97 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", + "Albums": "Albom", + "AppDeviceValues": "App: {0}, Grät: {1}", + "Application": "Aawändig", + "Artists": "Könstler", + "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet", "Books": "Büecher", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", + "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}", + "Channels": "Kanäu", + "ChapterNameValue": "Kapitu {0}", + "Collections": "Sammlige", + "DeviceOfflineWithName": "{0} esch offline gange", + "DeviceOnlineWithName": "{0} esch online cho", + "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}", + "Favorites": "Favorite", + "Folders": "Ordner", "Genres": "Genres", - "HeaderAlbumArtists": "Albuminterprete", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Albom-Könstler", + "HeaderCameraUploads": "Kamera-Uploads", "HeaderContinueWatching": "Wiiterluege", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Besti Interpret", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Besti Lieder", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", + "HeaderFavoriteAlbums": "Lieblingsalbe", + "HeaderFavoriteArtists": "Lieblings-Interprete", + "HeaderFavoriteEpisodes": "Lieblingsepisode", + "HeaderFavoriteShows": "Lieblingsserie", + "HeaderFavoriteSongs": "Lieblingslieder", + "HeaderLiveTV": "Live-Färnseh", + "HeaderNextUp": "Als nächts", "HeaderRecordingGroups": "Ufnahmegruppe", "HomeVideos": "Heimfilmli", "Inherit": "Hinzuefüege", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Letschte", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Gmischte Inhalt", - "Movies": "Movies", + "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde", + "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde", + "LabelIpAddressValue": "IP-Adrässe: {0}", + "LabelRunningTimeValue": "Loufziit: {0}", + "Latest": "Nöischti", + "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde", + "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde", + "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde", + "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde", + "MixedContent": "Gmeschti Inhäut", + "Movies": "Film", "Music": "Musig", - "MusicVideos": "Musigfilm", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "MusicVideos": "Musigvideos", + "NameInstallFailed": "Installation vo {0} fäugschlage", + "NameSeasonNumber": "Staffle {0}", + "NameSeasonUnknown": "Staffle unbekannt", + "NewVersionIsAvailable": "E nöi Version vo Jellyfin Server esch zom Download parat.", + "NotificationOptionApplicationUpdateAvailable": "Aawändigsupdate verfüegbar", + "NotificationOptionApplicationUpdateInstalled": "Aawändigsupdate installiert", + "NotificationOptionAudioPlayback": "Audiowedergab gstartet", + "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt", + "NotificationOptionCameraImageUploaded": "Foti ueglade", + "NotificationOptionInstallationFailed": "Installationsfäuer", + "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt", + "NotificationOptionPluginError": "Plugin-Fäuer", + "NotificationOptionPluginInstalled": "Plugin installiert", + "NotificationOptionPluginUninstalled": "Plugin deinstalliert", + "NotificationOptionPluginUpdateInstalled": "Pluginupdate installiert", + "NotificationOptionServerRestartRequired": "Serverneustart notwändig", + "NotificationOptionTaskFailed": "Planti Uufgab fäugschlage", + "NotificationOptionUserLockedOut": "Benotzer usgschlosse", + "NotificationOptionVideoPlayback": "Videowedergab gstartet", + "NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt", "Photos": "Fotis", - "Playlists": "Abspielliste", + "Playlists": "Wedergabeliste", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "PluginInstalledWithName": "{0} esch installiert worde", + "PluginUninstalledWithName": "{0} esch deinstalliert worde", + "PluginUpdatedWithName": "{0} esch updated worde", + "ProviderValue": "Aabieter: {0}", + "ScheduledTaskFailedWithName": "{0} esch fäugschlage", + "ScheduledTaskStartedWithName": "{0} het gstartet", + "ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde", + "Shows": "Serie", + "Songs": "Lieder", + "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", + "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde", + "SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}", + "Sync": "Synchronisation", "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Spezial - {0}", + "TvShows": "Färnsehserie", + "User": "Benotzer", + "UserCreatedWithName": "Benotzer {0} esch erstöut worde", + "UserDeletedWithName": "Benotzer {0} esch glösche worde", + "UserDownloadingItemWithValues": "{0} ladt {1} abe", + "UserLockedOutWithName": "Benotzer {0} esch usgschlosse worde", + "UserOfflineFromDevice": "{0} esch vo {1} trennt worde", + "UserOnlineFromDevice": "{0} esch online vo {1}", + "UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde", + "UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde", + "UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet", + "UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt", + "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde", + "ValueSpecialEpisodeName": "Extra - {0}", "VersionNumber": "Version {0}" } From 4a6243096afa9215392c4b2671c84e76eaf0d0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Libor=20Fil=C3=ADpek?= Date: Fri, 19 Apr 2019 17:30:29 +0000 Subject: [PATCH 0349/6208] Translated using Weblate (Czech) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index c2a101a4da..c19148921b 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -19,10 +19,10 @@ "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovat ve sledování", "HeaderFavoriteAlbums": "Oblíbená alba", - "HeaderFavoriteArtists": "Oblíbení umělci", + "HeaderFavoriteArtists": "Oblíbení interpreti", "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", - "HeaderFavoriteSongs": "Oblíbené písně", + "HeaderFavoriteSongs": "Oblíbená hudba", "HeaderLiveTV": "Live TV", "HeaderNextUp": "Nadcházející", "HeaderRecordingGroups": "Skupiny nahrávek", From 0794a3edf48eb232030560c4a03587cd77f38b65 Mon Sep 17 00:00:00 2001 From: bugfixin Date: Thu, 18 Apr 2019 21:47:19 +0000 Subject: [PATCH 0350/6208] Adjust detection of 'sample' in filenames to use regex boundaries --- .../Library/CoreResolutionIgnoreRule.cs | 11 +++-------- .../Library/Resolvers/Movies/MovieResolver.cs | 13 +++---------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index c644d13eab..c9d4e43422 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -148,15 +149,9 @@ namespace Emby.Server.Implementations.Library } // Ignore samples - var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) - .Replace("-", " ", StringComparison.OrdinalIgnoreCase) - .Replace("_", " ", StringComparison.OrdinalIgnoreCase) - .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + Match m = Regex.Match(filename,"\bsample\b",RegexOptions.IgnoreCase); - if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } + return m.Success; } return false; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 8485636794..47c3e71d72 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Emby.Naming.Video; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -167,17 +168,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static bool IsIgnored(string filename) { // Ignore samples - var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) - .Replace("-", " ", StringComparison.OrdinalIgnoreCase) - .Replace("_", " ", StringComparison.OrdinalIgnoreCase) - .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase); - if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return false; + return m.Success; } private bool ContainsFile(List result, FileSystemMetadata file) From 9d60cc8c660c47df110d8118975410f86a5a7484 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 19 Apr 2019 13:59:04 -0400 Subject: [PATCH 0351/6208] Rename Chinese (Traditional) file --- .../Localization/Core/{zh_Hant.json => zh-TW.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Emby.Server.Implementations/Localization/Core/{zh_Hant.json => zh-TW.json} (100%) diff --git a/Emby.Server.Implementations/Localization/Core/zh_Hant.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json similarity index 100% rename from Emby.Server.Implementations/Localization/Core/zh_Hant.json rename to Emby.Server.Implementations/Localization/Core/zh-TW.json From 46c37c0ae8d5b5de02ff6f8208a7b91436f54237 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 19 Apr 2019 14:25:29 -0400 Subject: [PATCH 0352/6208] Bump version to 10.3.0 (release) --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- MediaBrowser.WebDashboard/jellyfin-web | 2 +- build.yaml | 2 +- deployment/debian-package-x64/pkg-src/changelog | 12 +++--------- deployment/fedora-package-x64/pkg-src/jellyfin.spec | 6 ++---- 7 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36fc43983e..050f8fc49f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update \ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 +ARG JELLYFIN_WEB_VERSION=10.3.0 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm b/Dockerfile.arm index c896d83f27..81089b5196 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -30,7 +30,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 +ARG JELLYFIN_WEB_VERSION=10.3.0 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 09c9dc12f8..1ad4dc5a28 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -31,7 +31,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 +ARG JELLYFIN_WEB_VERSION=10.3.0 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index e07edea5bf..874b51234e 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit e07edea5bf22c253fc7ee91f45879d8ee2d1bf17 +Subproject commit 874b51234ee4e1f01e2e7410980a1003f316d6a2 diff --git a/build.yaml b/build.yaml index ccd7ce8f85..47ca589f8b 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.3.0-rc2" +version: "10.3.0" packages: - debian-package-x64 - debian-package-armhf diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 65880620dc..3908c277b3 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,14 +1,8 @@ -jellyfin (10.3.0~rc2) unstable; urgency=medium +jellyfin (10.3.0-1) unstable; urgency=medium - * New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 + * New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 - -- Jellyfin Packaging Team Wed, 10 Apr 2019 00:51:14 -0400 - -jellyfin (10.3.0~rc1) unstable; urgency=medium - - * New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 - - -- Jellyfin Packaging Team Sat, 30 Mar 2019 15:47:24 -0400 + -- Jellyfin Packaging Team Fri, 19 Apr 2019 14:24:29 -0400 jellyfin (10.2.2-1) unstable; urgency=medium diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 581d926fbf..36fe78c628 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -140,10 +140,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog -* Wed Apr 10 2019 Jellyfin Packaging Team -- New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 -* Sat Mar 30 2019 Jellyfin Packaging Team -- New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 +* Fri Apr 19 2019 Jellyfin Packaging Team +- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 * Thu Feb 28 2019 Jellyfin Packaging Team - jellyfin: - PR968 Release 10.2.z copr autobuild From da842d5a730ea8db2fab8e4c71a501191d54e46c Mon Sep 17 00:00:00 2001 From: bugfixin Date: Fri, 19 Apr 2019 18:35:28 +0000 Subject: [PATCH 0353/6208] Fix incorrect escaping in regex pattern --- Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index c9d4e43422..a70077163e 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Library } // Ignore samples - Match m = Regex.Match(filename,"\bsample\b",RegexOptions.IgnoreCase); + Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase); return m.Success; } From f62af07381b633d8e7ddf5787d9048fbbf4e3c85 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 20 Apr 2019 12:18:44 +0200 Subject: [PATCH 0354/6208] Handle exception when loading unsupported assembly Fixes #1256 --- Emby.Server.Implementations/ApplicationHost.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 82042f5ca7..0295f10981 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1167,7 +1167,7 @@ namespace Emby.Server.Implementations } catch (Exception ex) { - Logger.LogError(ex, "Error loading plugin {pluginName}", plugin.GetType().FullName); + Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName); return null; } @@ -1348,8 +1348,19 @@ namespace Emby.Server.Implementations { foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories)) { - Logger.LogInformation("Loading assembly {Path}", file); - yield return Assembly.LoadFrom(file); + Assembly plugAss; + try + { + plugAss = Assembly.LoadFrom(file); + } + catch (TypeLoadException ex) + { + Logger.LogError(ex, "Failed to load assembly {Path}", file); + continue; + } + + Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); + yield return plugAss; } } From 6973182ade7af9173abaf835608327be30b6b162 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 20 Apr 2019 14:02:00 +0200 Subject: [PATCH 0355/6208] Fix more possible exceptions --- .../ApplicationHost.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0295f10981..5d5a63a635 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1181,10 +1181,32 @@ namespace Emby.Server.Implementations { Logger.LogInformation("Loading assemblies"); - AllConcreteTypes = GetComposablePartAssemblies() - .SelectMany(x => x.ExportedTypes) - .Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType) - .ToArray(); + AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); + } + + private IEnumerable GetTypes(IEnumerable assemblies) + { + foreach (var ass in assemblies) + { + Type[] exportedTypes; + try + { + exportedTypes = ass.GetExportedTypes(); + } + catch (TypeLoadException ex) + { + Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); + continue; + } + + foreach (Type type in exportedTypes) + { + if (type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType) + { + yield return type; + } + } + } } private CertificateInfo CertificateInfo { get; set; } @@ -1353,7 +1375,7 @@ namespace Emby.Server.Implementations { plugAss = Assembly.LoadFrom(file); } - catch (TypeLoadException ex) + catch (FileLoadException ex) { Logger.LogError(ex, "Failed to load assembly {Path}", file); continue; From 764c6d5461ff359f50ab76a60589d871545eec2a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 20 Apr 2019 17:42:11 +0200 Subject: [PATCH 0356/6208] Fix comparison for empty password migration --- Emby.Server.Implementations/Data/SqliteUserRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index 182df0edc9..5957b29031 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data { // If the user password is the sha1 hash of the empty string, remove it if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) - || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) + && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) { continue; } From 5fb4922c6f0d01050bca109a2e066163da5863fa Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 20 Apr 2019 14:24:40 -0400 Subject: [PATCH 0357/6208] Bump version to 10.3.1 --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- SharedVersion.cs | 4 ++-- build.yaml | 2 +- deployment/debian-package-x64/pkg-src/changelog | 6 ++++++ deployment/fedora-package-x64/pkg-src/jellyfin.spec | 4 +++- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 050f8fc49f..3242003e16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update \ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0 +ARG JELLYFIN_WEB_VERSION=10.3.1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm b/Dockerfile.arm index 81089b5196..3cec9133f1 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -30,7 +30,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0 +ARG JELLYFIN_WEB_VERSION=10.3.1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 1ad4dc5a28..476ef2929d 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -31,7 +31,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.0 +ARG JELLYFIN_WEB_VERSION=10.3.1 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/SharedVersion.cs b/SharedVersion.cs index 3a0263bd67..9120f988cf 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.3.0")] -[assembly: AssemblyFileVersion("10.3.0")] +[assembly: AssemblyVersion("10.3.1")] +[assembly: AssemblyFileVersion("10.3.1")] diff --git a/build.yaml b/build.yaml index 47ca589f8b..0f843bf74a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.3.0" +version: "10.3.1" packages: - debian-package-x64 - debian-package-armhf diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 3908c277b3..ce30ca6f6d 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,9 @@ +jellyfin (10.3.1-1) unstable; urgency=medium + + * New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1 + + -- Jellyfin Packaging Team Sat, 20 Apr 2019 14:24:07 -0400 + jellyfin (10.3.0-1) unstable; urgency=medium * New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 36fe78c628..0d938aef96 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,7 +7,7 @@ %endif Name: jellyfin -Version: 10.3.0 +Version: 10.3.1 Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 @@ -140,6 +140,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Sat Apr 20 2019 Jellyfin Packaging Team +- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1 * Fri Apr 19 2019 Jellyfin Packaging Team - New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 * Thu Feb 28 2019 Jellyfin Packaging Team From 696a36b4a5ada69f00875afa5e1ff1c596c7a371 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sat, 20 Apr 2019 15:59:50 -0400 Subject: [PATCH 0358/6208] Update submodule for 10.3.1 --- MediaBrowser.WebDashboard/jellyfin-web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index 874b51234e..97f6808e12 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit 874b51234ee4e1f01e2e7410980a1003f316d6a2 +Subproject commit 97f6808e12243bbb9b58344185511a9369913c0b From 08d3a5d2feafe9d54c354028f38d7cfa4e94293c Mon Sep 17 00:00:00 2001 From: bugfixin Date: Sun, 21 Apr 2019 19:29:05 +0000 Subject: [PATCH 0359/6208] Fix null reference when request content type is application/x-www-form-urlencoded --- Emby.Server.Implementations/Services/ServiceHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 621be4fcb5..d32fce1c77 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -26,7 +26,10 @@ namespace Emby.Server.Implementations.Services if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) { var deserializer = RequestHelper.GetRequestReader(host, contentType); - return deserializer?.Invoke(requestType, httpReq.InputStream); + if (deserializer != null) + { + return deserializer.Invoke(requestType, httpReq.InputStream); + } } return Task.FromResult(host.CreateInstance(requestType)); From 28c2ac528d46ba97b920d37300fa814bd6f4a51a Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 24 Apr 2019 14:06:54 +0200 Subject: [PATCH 0360/6208] Re-add content length, semi revert of changes in #1010 (#1287) * Re-add content length, semi revert of changes in #1010 --- .../HttpServer/FileWriter.cs | 6 +- .../HttpServer/HttpListenerHost.cs | 1 + .../HttpServer/HttpResultFactory.cs | 13 ++- .../HttpServer/RangeRequestWriter.cs | 1 + .../HttpServer/ResponseFilter.cs | 3 +- .../HttpServer/StreamWriter.cs | 11 ++- .../Services/HttpResult.cs | 5 ++ .../Services/ResponseHelper.cs | 79 ++++++++----------- .../BaseProgressiveStreamingService.cs | 53 ++++++++++++- 9 files changed, 113 insertions(+), 59 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index c4d2a70e23..c6b7d31a83 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer if (string.IsNullOrWhiteSpace(rangeHeader)) { + Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); StatusCode = HttpStatusCode.OK; } else @@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; + // Content-Length is the length of what we're serving, not the original content + var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); + Headers[HeaderNames.ContentLength] = lengthString; var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = rangeString; - Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString); + Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); } /// diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 831391cee6..1fd27a7e36 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -638,6 +638,7 @@ namespace Emby.Server.Implementations.HttpServer private static Task Write(IResponse response, string text) { var bOutput = Encoding.UTF8.GetBytes(text); + response.OriginalResponse.ContentLength = bOutput.Length; return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 134f3c8418..5c29988c76 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer content = Array.Empty(); } - result = new StreamWriter(content, contentType); + result = new StreamWriter(content, contentType, contentLength); } else { @@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer bytes = Array.Empty(); } - result = new StreamWriter(bytes, contentType); + result = new StreamWriter(bytes, contentType, contentLength); } else { @@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer if (isHeadRequest) { - var result = new StreamWriter(Array.Empty(), contentType); + var result = new StreamWriter(Array.Empty(), contentType, contentLength); AddResponseHeaders(result, responseHeaders); return result; } else { - var result = new StreamWriter(content, contentType); + var result = new StreamWriter(content, contentType, contentLength); AddResponseHeaders(result, responseHeaders); return result; } @@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer } else { + if (totalContentLength.HasValue) + { + responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture); + } + if (isHeadRequest) { using (stream) diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 449159834a..e27f794ba6 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; + Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture); Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; if (RangeStart > 0 && SourceStream.CanSeek) diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index a53d9bf0b9..08f4247577 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer public void FilterResponse(IRequest req, IResponse res, object dto) { // Try to prevent compatibility view - res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); + res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Origin", "*"); @@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer if (length > 0) { + res.OriginalResponse.ContentLength = length; res.SendChunked = false; } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 324f9085e5..194d04441a 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -49,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer SourceStream = source; + Headers["Content-Type"] = contentType; + + if (source.CanSeek) + { + Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture); + } + Headers[HeaderNames.ContentType] = contentType; } @@ -57,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// The source. /// Type of the content. - public StreamWriter(byte[] source, string contentType) + public StreamWriter(byte[] source, string contentType, int contentLength) { if (string.IsNullOrEmpty(contentType)) { @@ -66,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer SourceBytes = source; + Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture); Headers[HeaderNames.ContentType] = contentType; } diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index b6758486ce..2b5963a770 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services { var contentLength = bytesResponse.Length; + if (response != null) + { + response.OriginalResponse.ContentLength = contentLength; + } + if (contentLength > 0) { await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index 0301ff335f..251ba3529a 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Net; using System.Text; @@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services { response.StatusCode = (int)HttpStatusCode.NoContent; } + + response.OriginalResponse.ContentLength = 0; return Task.CompletedTask; } @@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services response.StatusCode = httpResult.Status; response.StatusDescription = httpResult.StatusCode.ToString(); - //if (string.IsNullOrEmpty(httpResult.ContentType)) - //{ - // httpResult.ContentType = defaultContentType; - //} - //response.ContentType = httpResult.ContentType; } var responseOptions = result as IHasHeaders; @@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services { if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { + response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); continue; } @@ -72,52 +71,37 @@ namespace Emby.Server.Implementations.Services response.ContentType += "; charset=utf-8"; } - var asyncStreamWriter = result as IAsyncStreamWriter; - if (asyncStreamWriter != null) + switch (result) { - return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); - } + case IAsyncStreamWriter asyncStreamWriter: + return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); + case IStreamWriter streamWriter: + streamWriter.WriteTo(response.OutputStream); + return Task.CompletedTask; + case FileWriter fileWriter: + return fileWriter.WriteToAsync(response, cancellationToken); + case Stream stream: + return CopyStream(stream, response.OutputStream); + case byte[] bytes: + response.ContentType = "application/octet-stream"; + response.OriginalResponse.ContentLength = bytes.Length; - var streamWriter = result as IStreamWriter; - if (streamWriter != null) - { - streamWriter.WriteTo(response.OutputStream); - return Task.CompletedTask; - } + if (bytes.Length > 0) + { + return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); + } - var fileWriter = result as FileWriter; - if (fileWriter != null) - { - return fileWriter.WriteToAsync(response, cancellationToken); - } + return Task.CompletedTask; + case string responseText: + var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); + response.OriginalResponse.ContentLength = responseTextAsBytes.Length; - var stream = result as Stream; - if (stream != null) - { - return CopyStream(stream, response.OutputStream); - } + if (responseTextAsBytes.Length > 0) + { + return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); + } - var bytes = result as byte[]; - if (bytes != null) - { - response.ContentType = "application/octet-stream"; - - if (bytes.Length > 0) - { - return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - return Task.CompletedTask; - } - - var responseText = result as string; - if (responseText != null) - { - bytes = Encoding.UTF8.GetBytes(responseText); - if (bytes.Length > 0) - { - return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - return Task.CompletedTask; + return Task.CompletedTask; } return WriteObject(request, result, response); @@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services ms.Position = 0; var contentLength = ms.Length; + response.OriginalResponse.ContentLength = contentLength; if (contentLength > 0) { await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); } } - - //serializer(result, outputStream); } } } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index a2c20e38fd..1c36289c5b 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Playback.Progressive @@ -279,10 +281,9 @@ namespace MediaBrowser.Api.Playback.Progressive /// Task{System.Object}. private async Task GetStaticRemoteStreamResult(StreamState state, Dictionary responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource) { - string useragent = null; - state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); + state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent); - var trySupportSeek = false; + const bool trySupportSeek = false; var options = new HttpRequestOptions { @@ -317,6 +318,12 @@ namespace MediaBrowser.Api.Playback.Progressive responseHeaders[HeaderNames.AcceptRanges] = "none"; } + // Seeing cases of -1 here + if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) + { + responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture); + } + if (isHeadRequest) { using (response) @@ -356,10 +363,31 @@ namespace MediaBrowser.Api.Playback.Progressive var contentType = state.GetMimeType(outputPath); // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response + var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null; + + if (contentLength.HasValue) + { + responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture); + } + // Headers only if (isHeadRequest) { - return ResultFactory.GetResult(null, Array.Empty(), contentType, responseHeaders); + var streamResult = ResultFactory.GetResult(null, Array.Empty(), contentType, responseHeaders); + + if (streamResult is IHasHeaders hasHeaders) + { + if (contentLength.HasValue) + { + hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + hasHeaders.Headers.Remove(HeaderNames.ContentLength); + } + } + + return streamResult; } var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); @@ -397,5 +425,22 @@ namespace MediaBrowser.Api.Playback.Progressive transcodingLock.Release(); } } + + /// + /// Gets the length of the estimated content. + /// + /// The state. + /// System.Nullable{System.Int64}. + private long? GetEstimatedContentLength(StreamState state) + { + var totalBitrate = state.TotalOutputBitrate ?? 0; + + if (totalBitrate > 0 && state.RunTimeTicks.HasValue) + { + return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); + } + + return null; + } } } From a9337033c1d95d7238e9411abbc7255ef2456f35 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 24 Apr 2019 15:25:22 +0200 Subject: [PATCH 0361/6208] Fix query time logging --- .../Data/SqliteItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 088a6694b0..8841a9a504 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2741,15 +2741,16 @@ namespace Emby.Server.Implementations.Data { var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; - int slowThreshold = 100; - #if DEBUG - slowThreshold = 10; + const int SlowThreshold = 100; +#else + const int SlowThreshold = 10; #endif - if (elapsed >= slowThreshold) + if (elapsed >= SlowThreshold) { - Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}", + Logger.LogWarning( + "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", methodName, elapsed, commandText); From 71479286e9b35cd43b78037d551da13cc19e49bd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 24 Apr 2019 19:38:14 +0200 Subject: [PATCH 0362/6208] Fix #1234 --- Emby.Server.Implementations/HttpServer/HttpResultFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 5c29988c76..0b2924a3ba 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -629,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer if (lastModifiedDate.HasValue) { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); + responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture); } } From 844ea9d77ed24bb5b3bc5ec5b23df8a045933759 Mon Sep 17 00:00:00 2001 From: bugfixin Date: Thu, 25 Apr 2019 04:36:28 +0000 Subject: [PATCH 0363/6208] Don't coalesce empty strings to null in StringMapTypeDeserializer --- .../Services/StringMapTypeDeserializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index f835aa1b5b..6a522fbef3 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services string propertyName = pair.Key; string propertyTextValue = pair.Value; - if (string.IsNullOrEmpty(propertyTextValue) + if (propertyTextValue == null || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) || propertySerializerEntry.PropertySetFn == null) { From a827a2fbccd0532631f4de849e18a9eeff772b5d Mon Sep 17 00:00:00 2001 From: bugfixin Date: Thu, 25 Apr 2019 19:14:33 +0000 Subject: [PATCH 0364/6208] Remove unreachable code and const trySupportSeek within BaseProgressiveStreamingService --- .../BaseProgressiveStreamingService.cs | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 1c36289c5b..83a3f3e3c6 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -283,8 +283,6 @@ namespace MediaBrowser.Api.Playback.Progressive { state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent); - const bool trySupportSeek = false; - var options = new HttpRequestOptions { Url = state.MediaPath, @@ -293,30 +291,9 @@ namespace MediaBrowser.Api.Playback.Progressive CancellationToken = cancellationTokenSource.Token }; - if (trySupportSeek) - { - if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range])) - { - options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range]; - } - } var response = await HttpClient.GetResponse(options).ConfigureAwait(false); - if (trySupportSeek) - { - foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges }) - { - var val = response.Headers[name]; - if (!string.IsNullOrWhiteSpace(val)) - { - responseHeaders[name] = val; - } - } - } - else - { - responseHeaders[HeaderNames.AcceptRanges] = "none"; - } + responseHeaders[HeaderNames.AcceptRanges] = "none"; // Seeing cases of -1 here if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) From 2b2a2ed70892e1e2ed55e59408df530fcdc01933 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Mon, 29 Apr 2019 00:56:17 -0400 Subject: [PATCH 0365/6208] Add arm64 packaging for Debuntu --- build.yaml | 2 + .../debian-package-arm64/Dockerfile.amd64 | 43 +++++++++++++++ .../debian-package-arm64/Dockerfile.arm64 | 34 ++++++++++++ deployment/debian-package-arm64/clean.sh | 29 ++++++++++ .../debian-package-arm64/dependencies.txt | 1 + .../debian-package-arm64/docker-build.sh | 20 +++++++ deployment/debian-package-arm64/package.sh | 42 +++++++++++++++ deployment/debian-package-arm64/pkg-src | 1 + deployment/debian-package-x64/pkg-src/rules | 13 +++-- .../ubuntu-package-arm64/Dockerfile.amd64 | 53 +++++++++++++++++++ .../ubuntu-package-arm64/Dockerfile.arm64 | 34 ++++++++++++ deployment/ubuntu-package-arm64/clean.sh | 29 ++++++++++ .../ubuntu-package-arm64/dependencies.txt | 1 + .../ubuntu-package-arm64/docker-build.sh | 20 +++++++ deployment/ubuntu-package-arm64/package.sh | 42 +++++++++++++++ deployment/ubuntu-package-arm64/pkg-src | 1 + 16 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 deployment/debian-package-arm64/Dockerfile.amd64 create mode 100644 deployment/debian-package-arm64/Dockerfile.arm64 create mode 100755 deployment/debian-package-arm64/clean.sh create mode 100644 deployment/debian-package-arm64/dependencies.txt create mode 100755 deployment/debian-package-arm64/docker-build.sh create mode 100755 deployment/debian-package-arm64/package.sh create mode 120000 deployment/debian-package-arm64/pkg-src create mode 100644 deployment/ubuntu-package-arm64/Dockerfile.amd64 create mode 100644 deployment/ubuntu-package-arm64/Dockerfile.arm64 create mode 100755 deployment/ubuntu-package-arm64/clean.sh create mode 100644 deployment/ubuntu-package-arm64/dependencies.txt create mode 100755 deployment/ubuntu-package-arm64/docker-build.sh create mode 100755 deployment/ubuntu-package-arm64/package.sh create mode 120000 deployment/ubuntu-package-arm64/pkg-src diff --git a/build.yaml b/build.yaml index 0f843bf74a..d6bb106d32 100644 --- a/build.yaml +++ b/build.yaml @@ -5,8 +5,10 @@ version: "10.3.1" packages: - debian-package-x64 - debian-package-armhf + - debian-package-arm64 - ubuntu-package-x64 - ubuntu-package-armhf + - ubuntu-package-arm64 - fedora-package-x64 - centos-package-x64 - linux-x64 diff --git a/deployment/debian-package-arm64/Dockerfile.amd64 b/deployment/debian-package-arm64/Dockerfile.amd64 new file mode 100644 index 0000000000..2cb8bd856e --- /dev/null +++ b/deployment/debian-package-arm64/Dockerfile.amd64 @@ -0,0 +1,43 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Prepare the cross-toolchain +RUN dpkg --add-architecture arm64 \ + && apt-get update \ + && apt-get install -y cross-gcc-dev \ + && TARGET_LIST="arm64" cross-gcc-gensource 6 \ + && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \ + && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] +#ENTRYPOINT ["/bin/bash"] diff --git a/deployment/debian-package-arm64/Dockerfile.arm64 b/deployment/debian-package-arm64/Dockerfile.arm64 new file mode 100644 index 0000000000..bb9e28d0aa --- /dev/null +++ b/deployment/debian-package-arm64/Dockerfile.arm64 @@ -0,0 +1,34 @@ +FROM debian:9 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=arm64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-arm64/clean.sh b/deployment/debian-package-arm64/clean.sh new file mode 100755 index 0000000000..ac143c3d0b --- /dev/null +++ b/deployment/debian-package-arm64/clean.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +keep_artifacts="${1}" + +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_arm64-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/debian-package-arm64/dependencies.txt b/deployment/debian-package-arm64/dependencies.txt new file mode 100644 index 0000000000..bdb9670965 --- /dev/null +++ b/deployment/debian-package-arm64/dependencies.txt @@ -0,0 +1 @@ +docker diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh new file mode 100755 index 0000000000..308f3df15b --- /dev/null +++ b/deployment/debian-package-arm64/docker-build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Builds the DEB inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image +sed -i '/dotnet-sdk-2.2,/d' debian/control + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -aarm64 + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/deb +mv /jellyfin_* ${ARTIFACT_DIR}/deb/ diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh new file mode 100755 index 0000000000..19f70d7f68 --- /dev/null +++ b/deployment/debian-package-arm64/package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +ARCH="$( arch )" +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-debian_arm64-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Determine which Dockerfile to use +case $ARCH in + 'x86_64') + DOCKERFILE="Dockerfile.amd64" + ;; + 'armv7l') + DOCKERFILE="Dockerfile.arm64" + ;; +esac + +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +# Correct ownership on the DEBs (as current user, then as root if that fails) +chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ + || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-arm64/pkg-src b/deployment/debian-package-arm64/pkg-src new file mode 120000 index 0000000000..4c695fea17 --- /dev/null +++ b/deployment/debian-package-arm64/pkg-src @@ -0,0 +1 @@ +../debian-package-x64/pkg-src \ No newline at end of file diff --git a/deployment/debian-package-x64/pkg-src/rules b/deployment/debian-package-x64/pkg-src/rules index 62f75bc6b1..ee41e0e24b 100644 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/deployment/debian-package-x64/pkg-src/rules @@ -6,18 +6,25 @@ SHELL := /bin/bash HOST_ARCH := $(shell arch) BUILD_ARCH := ${DEB_HOST_MULTIARCH} ifeq ($(HOST_ARCH),x86_64) + # Building AMD64 + DOTNETRUNTIME := debian-x64 ifeq ($(BUILD_ARCH),arm-linux-gnueabihf) # Cross-building ARM on AMD64 DOTNETRUNTIME := debian-arm - else - # Building AMD64 - DOTNETRUNTIME := debian-x64 + endif + ifeq ($(BUILD_ARCH),aarch64-linux-gnu) + # Cross-building ARM on AMD64 + DOTNETRUNTIME := debian-arm64 endif endif ifeq ($(HOST_ARCH),armv7l) # Building ARM DOTNETRUNTIME := debian-arm endif +ifeq ($(HOST_ARCH),arm64) + # Building ARM + DOTNETRUNTIME := debian-arm64 +endif export DH_VERBOSE=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1 diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/ubuntu-package-arm64/Dockerfile.amd64 new file mode 100644 index 0000000000..5e51ef0f02 --- /dev/null +++ b/deployment/ubuntu-package-arm64/Dockerfile.amd64 @@ -0,0 +1,53 @@ +FROM ubuntu:bionic +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Prepare the cross-toolchain +RUN rm /etc/apt/sources.list \ + && export CODENAME="$( lsb_release -c -s )" \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && dpkg --add-architecture arm64 \ + && apt-get update \ + && apt-get install -y cross-gcc-dev \ + && TARGET_LIST="arm64" cross-gcc-gensource 6 \ + && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \ + && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ + && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64 new file mode 100644 index 0000000000..646679328c --- /dev/null +++ b/deployment/ubuntu-package-arm64/Dockerfile.arm64 @@ -0,0 +1,34 @@ +FROM ubuntu:bionic +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=arm64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-arm64/clean.sh b/deployment/ubuntu-package-arm64/clean.sh new file mode 100755 index 0000000000..c92c7fdec6 --- /dev/null +++ b/deployment/ubuntu-package-arm64/clean.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +keep_artifacts="${1}" + +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-ubuntu-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/ubuntu-package-arm64/dependencies.txt b/deployment/ubuntu-package-arm64/dependencies.txt new file mode 100644 index 0000000000..bdb9670965 --- /dev/null +++ b/deployment/ubuntu-package-arm64/dependencies.txt @@ -0,0 +1 @@ +docker diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh new file mode 100755 index 0000000000..308f3df15b --- /dev/null +++ b/deployment/ubuntu-package-arm64/docker-build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Builds the DEB inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image +sed -i '/dotnet-sdk-2.2,/d' debian/control + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -aarm64 + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/deb +mv /jellyfin_* ${ARTIFACT_DIR}/deb/ diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh new file mode 100755 index 0000000000..54fc387509 --- /dev/null +++ b/deployment/ubuntu-package-arm64/package.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +source ../common.build.sh + +ARCH="$( arch )" +WORKDIR="$( pwd )" + +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-ubuntu_arm64-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Determine which Dockerfile to use +case $ARCH in + 'x86_64') + DOCKERFILE="Dockerfile.amd64" + ;; + 'armv7l') + DOCKERFILE="Dockerfile.arm64" + ;; +esac + +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +# Correct ownership on the DEBs (as current user, then as root if that fails) +chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ + || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-arm64/pkg-src b/deployment/ubuntu-package-arm64/pkg-src new file mode 120000 index 0000000000..4c695fea17 --- /dev/null +++ b/deployment/ubuntu-package-arm64/pkg-src @@ -0,0 +1 @@ +../debian-package-x64/pkg-src \ No newline at end of file From c8a59c83439180ac2b4795d8d09009a30e696e93 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Mon, 29 Apr 2019 23:03:57 -0400 Subject: [PATCH 0366/6208] Support libssl1.1 for Ubuntu Disco --- deployment/debian-package-x64/pkg-src/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control index d96660590c..4422f0fda4 100644 --- a/deployment/debian-package-x64/pkg-src/control +++ b/deployment/debian-package-x64/pkg-src/control @@ -23,6 +23,6 @@ Depends: at, jellyfin-ffmpeg, libfontconfig1, libfreetype6, - libssl1.0.0 | libssl1.0.2 + libssl1.0.0 | libssl1.0.2 | libssl1.1 Description: Jellyfin is a home media server. It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development. From 08ed52eb726f03098bdf3218ee9f9d568880ccd0 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 30 Apr 2019 20:08:59 +0200 Subject: [PATCH 0367/6208] Make the TvdbEpisodeProvider class Public --- MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index 5474a7c398..302d40c6b7 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB /// /// Class RemoteEpisodeProvider /// - class TvdbEpisodeProvider : IRemoteMetadataProvider, IHasOrder + public class TvdbEpisodeProvider : IRemoteMetadataProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; From 1df73fdeba0aca5ff2835080659877f0a6722f17 Mon Sep 17 00:00:00 2001 From: bugfixin Date: Tue, 30 Apr 2019 19:16:53 +0000 Subject: [PATCH 0368/6208] Fix incorrect hasPassword flag when easy pin set --- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 952cc6896b..c33bb77409 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -596,7 +596,7 @@ namespace Emby.Server.Implementations.Library } bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetLocalPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : From 682432f55a4cdb24fb2ca47ec4c8667014be2ad7 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 30 Apr 2019 22:18:40 +0200 Subject: [PATCH 0369/6208] Iterate over IEnumerable before disposing --- MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs | 2 +- MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 3797f9039a..179e953f48 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music } using (var subReader = reader.ReadSubtree()) { - return ParseReleaseList(subReader); + return ParseReleaseList(subReader).ToList(); } } default: diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs index 59280df897..728f7731af 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs @@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music } using (var subReader = reader.ReadSubtree()) { - return ParseArtistList(subReader); + return ParseArtistList(subReader).ToList(); } } default: From 91cd7d2f6b3e9ce1212d9c29519aa2b23731b8e9 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 30 Apr 2019 23:35:39 +0200 Subject: [PATCH 0370/6208] Limit amount of ffmpeg processes extracting images at once --- .../Encoder/MediaEncoder.cs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index b626600fa4..a8874b6d0c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly int DefaultImageExtractionTimeoutMs; private readonly string StartupOptionFFmpegPath; - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); private readonly List _runningProcesses = new List(); private readonly ILocalizationManager _localization; @@ -582,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder { bool ranToCompletion; - StartProcess(processWrapper); - - var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; - if (timeoutMs <= 0) + await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - timeoutMs = DefaultImageExtractionTimeoutMs; + StartProcess(processWrapper); + + var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; + if (timeoutMs <= 0) + { + timeoutMs = DefaultImageExtractionTimeoutMs; + } + + ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); + + if (!ranToCompletion) + { + StopProcess(processWrapper, 1000); + } } - - ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); - - if (!ranToCompletion) + finally { - StopProcess(processWrapper, 1000); + _thumbnailResourcePool.Release(); } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; @@ -625,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); } - public async Task ExtractVideoImagesOnInterval(string[] inputFiles, + public async Task ExtractVideoImagesOnInterval( + string[] inputFiles, string container, MediaStream videoStream, MediaProtocol protocol, @@ -636,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder int? maxWidth, CancellationToken cancellationToken) { - var resourcePool = _thumbnailResourcePool; - var inputArgument = GetInputArgument(inputFiles, protocol); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture); @@ -701,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); bool ranToCompletion = false; @@ -742,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } finally { - resourcePool.Release(); + _thumbnailResourcePool.Release(); } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; From e8196fed7cdc43f83f666af477652a90f41b5961 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 30 Apr 2019 20:18:54 -0400 Subject: [PATCH 0371/6208] Bump version for 10.3.2 --- Dockerfile | 2 +- Dockerfile.arm | 2 +- Dockerfile.arm64 | 2 +- MediaBrowser.WebDashboard/jellyfin-web | 2 +- SharedVersion.cs | 4 ++-- build.yaml | 2 +- deployment/debian-package-x64/pkg-src/changelog | 6 ++++++ deployment/fedora-package-x64/pkg-src/jellyfin.spec | 4 +++- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3242003e16..f414995f79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update \ COPY --from=ffmpeg / / COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.1 +ARG JELLYFIN_WEB_VERSION=10.3.2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm b/Dockerfile.arm index 3cec9133f1..66f731354b 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -30,7 +30,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.1 +ARG JELLYFIN_WEB_VERSION=10.3.2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 476ef2929d..690d4b6e7a 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -31,7 +31,7 @@ RUN apt-get update \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin -ARG JELLYFIN_WEB_VERSION=10.3.1 +ARG JELLYFIN_WEB_VERSION=10.3.2 RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && rm -rf /jellyfin/jellyfin-web \ && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web index 97f6808e12..1ba58b06b3 160000 --- a/MediaBrowser.WebDashboard/jellyfin-web +++ b/MediaBrowser.WebDashboard/jellyfin-web @@ -1 +1 @@ -Subproject commit 97f6808e12243bbb9b58344185511a9369913c0b +Subproject commit 1ba58b06b3dc28e07abae124cff78aa656fcb7e7 diff --git a/SharedVersion.cs b/SharedVersion.cs index 9120f988cf..700ef45491 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.3.1")] -[assembly: AssemblyFileVersion("10.3.1")] +[assembly: AssemblyVersion("10.3.2")] +[assembly: AssemblyFileVersion("10.3.2")] diff --git a/build.yaml b/build.yaml index d6bb106d32..8ce6a00a80 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.3.1" +version: "10.3.2" packages: - debian-package-x64 - debian-package-armhf diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index ce30ca6f6d..61977c4e08 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,9 @@ +jellyfin (10.3.2-1) unstable; urgency=medium + + * New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2 + + -- Jellyfin Packaging Team Tue, 30 Apr 2019 20:18:44 -0400 + jellyfin (10.3.1-1) unstable; urgency=medium * New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1 diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 0d938aef96..58d6435696 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,7 +7,7 @@ %endif Name: jellyfin -Version: 10.3.1 +Version: 10.3.2 Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 @@ -140,6 +140,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Tue Apr 30 2019 Jellyfin Packaging Team +- New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2 * Sat Apr 20 2019 Jellyfin Packaging Team - New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1 * Fri Apr 19 2019 Jellyfin Packaging Team From c1daea0ec7c07675b8a4c3f038be69d94a36a794 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 1 May 2019 07:47:22 +0200 Subject: [PATCH 0372/6208] Change owner and parent id of extras to the main media item --- MediaBrowser.Controller/Entities/BaseItem.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e20641c99a..b05e97868f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1481,7 +1481,10 @@ namespace MediaBrowser.Controller.Entities private async Task RefreshExtras(BaseItem item, MetadataRefreshOptions options, List fileSystemChildren, CancellationToken cancellationToken) { - var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)); + var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService) + .Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)) + .Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)) + .ToArray(); var newExtraIds = newExtras.Select(i => i.Id).ToArray(); @@ -1493,7 +1496,17 @@ namespace MediaBrowser.Controller.Entities var tasks = newExtras.Select(i => { - return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); + var subOptions = new MetadataRefreshOptions(options); + if (!i.ExtraType.HasValue || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); await Task.WhenAll(tasks).ConfigureAwait(false); From 3634d367c1809a2ff53b0583bd19b87e860d966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Thu, 25 Apr 2019 05:33:17 +0200 Subject: [PATCH 0373/6208] Move artifact chown inside docker to avoid sudo --- deployment/centos-package-x64/docker-build.sh | 1 + deployment/centos-package-x64/package.sh | 3 --- deployment/debian-package-arm64/docker-build.sh | 1 + deployment/debian-package-arm64/package.sh | 5 ++--- deployment/debian-package-armhf/docker-build.sh | 1 + deployment/debian-package-armhf/package.sh | 5 ++--- deployment/debian-package-x64/docker-build.sh | 1 + deployment/debian-package-x64/package.sh | 5 ++--- deployment/fedora-package-x64/docker-build.sh | 1 + deployment/fedora-package-x64/package.sh | 5 ++--- deployment/ubuntu-package-arm64/docker-build.sh | 1 + deployment/ubuntu-package-arm64/package.sh | 5 ++--- deployment/ubuntu-package-armhf/docker-build.sh | 1 + deployment/ubuntu-package-armhf/package.sh | 5 ++--- deployment/ubuntu-package-x64/docker-build.sh | 1 + deployment/ubuntu-package-x64/package.sh | 5 ++--- 16 files changed, 22 insertions(+), 24 deletions(-) diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh index 3acf1ec0df..cefb1652e9 100755 --- a/deployment/centos-package-x64/docker-build.sh +++ b/deployment/centos-package-x64/docker-build.sh @@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg- # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/rpm mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/centos-package-x64/package.sh b/deployment/centos-package-x64/package.sh index 27d686e46f..df5a665808 100755 --- a/deployment/centos-package-x64/package.sh +++ b/deployment/centos-package-x64/package.sh @@ -72,9 +72,6 @@ fi ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the RPMs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the RPMs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" # Move the RPMs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh index 308f3df15b..cee96e1369 100755 --- a/deployment/debian-package-arm64/docker-build.sh +++ b/deployment/debian-package-arm64/docker-build.sh @@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarm64 # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh index 19f70d7f68..ce02b1af53 100755 --- a/deployment/debian-package-arm64/package.sh +++ b/deployment/debian-package-arm64/package.sh @@ -30,13 +30,12 @@ case $ARCH in ;; esac +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh index 45e68f0c6b..56227b5880 100755 --- a/deployment/debian-package-armhf/docker-build.sh +++ b/deployment/debian-package-armhf/docker-build.sh @@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh index 0ec0dc95cf..4393fb8340 100755 --- a/deployment/debian-package-armhf/package.sh +++ b/deployment/debian-package-armhf/package.sh @@ -30,13 +30,12 @@ case $ARCH in ;; esac +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh index 0590be0972..07f726dcc8 100755 --- a/deployment/debian-package-x64/docker-build.sh +++ b/deployment/debian-package-x64/docker-build.sh @@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-x64/package.sh b/deployment/debian-package-x64/package.sh index d7c3f5809c..2530e253bd 100755 --- a/deployment/debian-package-x64/package.sh +++ b/deployment/debian-package-x64/package.sh @@ -19,13 +19,12 @@ else docker_sudo="" fi +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh index 3acf1ec0df..cefb1652e9 100755 --- a/deployment/fedora-package-x64/docker-build.sh +++ b/deployment/fedora-package-x64/docker-build.sh @@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg- # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/rpm mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh index eed29aef3c..e659ee5e9e 100755 --- a/deployment/fedora-package-x64/package.sh +++ b/deployment/fedora-package-x64/package.sh @@ -23,13 +23,12 @@ fi ./create_tarball.sh +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the RPMs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the RPMs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" # Move the RPMs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh index 308f3df15b..cee96e1369 100755 --- a/deployment/ubuntu-package-arm64/docker-build.sh +++ b/deployment/ubuntu-package-arm64/docker-build.sh @@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarm64 # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh index 54fc387509..5a2bf61c86 100755 --- a/deployment/ubuntu-package-arm64/package.sh +++ b/deployment/ubuntu-package-arm64/package.sh @@ -30,13 +30,12 @@ case $ARCH in ;; esac +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh index 45e68f0c6b..56227b5880 100755 --- a/deployment/ubuntu-package-armhf/docker-build.sh +++ b/deployment/ubuntu-package-armhf/docker-build.sh @@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh index fb03652cd5..15f55bff20 100755 --- a/deployment/ubuntu-package-armhf/package.sh +++ b/deployment/ubuntu-package-armhf/package.sh @@ -30,13 +30,12 @@ case $ARCH in ;; esac +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-x64/docker-build.sh b/deployment/ubuntu-package-x64/docker-build.sh index 0590be0972..07f726dcc8 100755 --- a/deployment/ubuntu-package-x64/docker-build.sh +++ b/deployment/ubuntu-package-x64/docker-build.sh @@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/deb mv /jellyfin_* ${ARTIFACT_DIR}/deb/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-x64/package.sh b/deployment/ubuntu-package-x64/package.sh index 6d4625a197..32e6d4fd65 100755 --- a/deployment/ubuntu-package-x64/package.sh +++ b/deployment/ubuntu-package-x64/package.sh @@ -19,13 +19,12 @@ else docker_sudo="" fi +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the DEBs and copy out to ${package_temporary_dir} ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" -# Correct ownership on the DEBs (as current user, then as root if that fails) -chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \ - || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" From b8a09339cd7c14ce4806463c4d62b80f760d93aa Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 2 May 2019 08:14:00 +0200 Subject: [PATCH 0374/6208] Enforce extras folder structure according to Emby's wiki --- MediaBrowser.Controller/Entities/BaseItem.cs | 67 ++++++++++++-------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b05e97868f..1c6902b73f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -82,6 +82,21 @@ namespace MediaBrowser.Controller.Entities public static string ThemeSongsFolderName = "theme-music"; public static string ThemeSongFilename = "theme"; public static string ThemeVideosFolderName = "backdrops"; + public static string ExtrasFolderName = "extras"; + public static string BehindTheScenesFolderName = "behind the scenes"; + public static string DeletedScenesFolderName = "deleted scenes"; + public static string InterviewFolderName = "interviews"; + public static string SceneFolderName = "scenes"; + public static string SampleFolderName = "samples"; + + public static string[] AllExtrasTypesFolderNames = { + ExtrasFolderName, + BehindTheScenesFolderName, + DeletedScenesFolderName, + InterviewFolderName, + SceneFolderName, + SampleFolderName + }; [IgnoreDataMember] public Guid[] ThemeSongIds { get; set; } @@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities .Select(item => { // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - if (dbItem != null) + if (LibraryManager.GetItemById(item.Id) is Video dbItem) { item = dbItem; } else { // item is new - item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; + item.ExtraType = Model.Entities.ExtraType.ThemeVideo; } return item; @@ -1296,33 +1310,37 @@ namespace MediaBrowser.Controller.Entities protected virtual BaseItem[] LoadExtras(List fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.Where(i => i.IsDirectory) - .SelectMany(i => FileSystem.GetFiles(i.FullName)); + var extras = new List