diff options
Diffstat (limited to 'Emby.Server.Implementations/Library')
37 files changed, 537 insertions, 1867 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index bc1398332..77b2c0a69 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,7 +1,6 @@ using System; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -10,73 +9,48 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library { /// <summary> - /// Provides the core resolver ignore rules + /// Provides the core resolver ignore rules. /// </summary> public class CoreResolutionIgnoreRule : IResolverIgnoreRule { private readonly ILibraryManager _libraryManager; - - /// <summary> - /// Any folder named in this list will be ignored - /// </summary> - private static readonly string[] _ignoreFolders = - { - "metadata", - "ps3_update", - "ps3_vprm", - "extrafanart", - "extrathumbs", - ".actors", - ".wd_tv", - - // Synology - "@eaDir", - "eaDir", - "#recycle", - - // Qnap - "@Recycle", - ".@__thumb", - "$RECYCLE.BIN", - "System Volume Information", - ".grab", - }; + private readonly IServerApplicationPaths _serverApplicationPaths; /// <summary> /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// </summary> /// <param name="libraryManager">The library manager.</param> - public CoreResolutionIgnoreRule(ILibraryManager libraryManager) + /// <param name="serverApplicationPaths">The server application paths.</param> + public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths) { _libraryManager = libraryManager; + _serverApplicationPaths = serverApplicationPaths; } /// <inheritdoc /> public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) { + // Don't ignore application folders + if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture)) + { + return false; + } + // Don't ignore top level folders if (fileInfo.IsDirectory && parent is AggregateFolder) { return false; } - var filename = fileInfo.Name; - - // Ignore hidden files on UNIX - if (Environment.OSVersion.Platform != PlatformID.Win32NT - && filename[0] == '.') + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) { return true; } + var filename = fileInfo.Name; + if (fileInfo.IsDirectory) { - // Ignore any folders in our list - if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (parent != null) { // Ignore trailer folders but allow it at the collection level @@ -109,11 +83,6 @@ namespace Emby.Server.Implementations.Library return true; } } - - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase); - - return m.Success; } return false; diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs deleted file mode 100644 index ab036eca7..000000000 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Library -{ - /// <summary> - /// The default authentication provider. - /// </summary> - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - - /// <summary> - /// Initializes a new instance of the <see cref="DefaultAuthenticationProvider"/> class. - /// </summary> - /// <param name="cryptographyProvider">The cryptography provider.</param> - public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) - { - _cryptographyProvider = cryptographyProvider; - } - - /// <inheritdoc /> - public string Name => "Default"; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - // 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<ProviderAuthenticationResult> Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - /// <inheritdoc /> - // This is the version that we need to use for local users. Because reasons. - public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) - { - if (resolvedUser == null) - { - throw new ArgumentNullException(nameof(resolvedUser)); - } - - bool success = false; - - // As long as jellyfin supports passwordless users, we need this little block here to accommodate - if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) - { - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) - || _cryptographyProvider.DefaultHashMethod == readyHash.Id) - { - byte[] calculatedHash = _cryptographyProvider.ComputeHash( - readyHash.Id, - passwordbytes, - readyHash.Salt.ToArray()); - - if (readyHash.Hash.SequenceEqual(calculatedHash)) - { - success = true; - } - } - else - { - throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); - } - - if (!success) - { - throw new AuthenticationException("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - /// <inheritdoc /> - public bool HasPassword(User user) - => !string.IsNullOrEmpty(user.Password); - - /// <inheritdoc /> - public Task ChangePassword(User user, string newPassword) - { - if (string.IsNullOrEmpty(newPassword)) - { - user.Password = null; - return Task.CompletedTask; - } - - PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); - user.Password = newPasswordHash.ToString(); - - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (newPassword != null) - { - newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - } - - /// <inheritdoc /> - public string GetEasyPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); - } - - /// <summary> - /// Gets the hashed string. - /// </summary> - public string GetHashedString(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).ToString(); - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - var salt = passwordHash.Salt.ToArray(); - return new PasswordHash( - passwordHash.Id, - _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - salt), - salt, - passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); - } - - public ReadOnlySpan<byte> GetHashed(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).Hash; - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - return _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - passwordHash.Salt.ToArray()); - } - } -} diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs deleted file mode 100644 index 6c6fbd86f..000000000 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Emby.Server.Implementations.Library -{ - /// <summary> - /// The default password reset provider. - /// </summary> - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - private const string BaseResetFileName = "passwordreset"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - - /// <summary> - /// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class. - /// </summary> - /// <param name="configurationManager">The configuration manager.</param> - /// <param name="jsonSerializer">The JSON serializer.</param> - /// <param name="userManager">The user manager.</param> - public DefaultPasswordResetProvider( - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - /// <inheritdoc /> - public string Name => "Default Password Reset Provider"; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) - { - SerializablePasswordReset spr; - List<string> usersreset = new List<string>(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) - { - using (var str = File.OpenRead(resetfile)) - { - spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false); - } - - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetfile); - } - else if (string.Equals( - spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), - pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.InvariantCultureIgnoreCase)) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser == null) - { - throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - } - - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - File.Delete(resetfile); - } - } - - 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() - }; - } - } - - /// <inheritdoc /> - public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) - { - string pin = string.Empty; - using (var cryptoRandom = 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"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Name - }; - - using (FileStream fileStream = File.OpenWrite(filePath)) - { - _jsonSerializer.SerializeToStream(spr, fileStream); - await fileStream.FlushAsync().ConfigureAwait(false); - } - - 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/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index 9a7186898..ab39a7223 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -12,11 +12,13 @@ namespace Emby.Server.Implementations.Library public class ExclusiveLiveStream : ILiveStream { public int ConsumerCount { get; set; } + public string OriginalStreamId { get; set; } public string TunerHostId => null; public bool EnableStreamSharing { get; set; } + public MediaSourceInfo MediaSource { get; set; } public string UniqueId { get; private set; } diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs new file mode 100644 index 000000000..6e6ef1359 --- /dev/null +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -0,0 +1,108 @@ +#nullable enable + +using System; +using System.Linq; +using DotNet.Globbing; + +namespace Emby.Server.Implementations.Library +{ + /// <summary> + /// Glob patterns for files to ignore. + /// </summary> + public static class IgnorePatterns + { + /// <summary> + /// Files matching these glob patterns will be ignored. + /// </summary> + private static readonly string[] _patterns = + { + "**/small.jpg", + "**/albumart.jpg", + "**/*sample*", + + // Directories + "**/metadata/**", + "**/metadata", + "**/ps3_update/**", + "**/ps3_update", + "**/ps3_vprm/**", + "**/ps3_vprm", + "**/extrafanart/**", + "**/extrafanart", + "**/extrathumbs/**", + "**/extrathumbs", + "**/.actors/**", + "**/.actors", + "**/.wd_tv/**", + "**/.wd_tv", + "**/lost+found/**", + "**/lost+found", + + // WMC temp recording directories that will constantly be written to + "**/TempRec/**", + "**/TempRec", + "**/TempSBE/**", + "**/TempSBE", + + // Synology + "**/eaDir/**", + "**/eaDir", + "**/@eaDir/**", + "**/@eaDir", + "**/#recycle/**", + "**/#recycle", + + // Qnap + "**/@Recycle/**", + "**/@Recycle", + "**/.@__thumb/**", + "**/.@__thumb", + "**/$RECYCLE.BIN/**", + "**/$RECYCLE.BIN", + "**/System Volume Information/**", + "**/System Volume Information", + "**/.grab/**", + "**/.grab", + + // Unix hidden files and directories + "**/.*/**", + "**/.*", + + // thumbs.db + "**/thumbs.db", + + // bts sync files + "**/*.bts", + "**/*.sync", + }; + + private static readonly GlobOptions _globOptions = new GlobOptions + { + Evaluation = + { + CaseInsensitive = true + } + }; + + private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); + + /// <summary> + /// Returns true if the supplied path should be ignored. + /// </summary> + /// <param name="path">The path to test.</param> + /// <returns>Whether to ignore the path.</returns> + public static bool ShouldIgnore(ReadOnlySpan<char> path) + { + int len = _globs.Length; + for (int i = 0; i < len; i++) + { + if (_globs[i].IsMatch(path)) + { + return true; + } + } + + return false; + } + } +} diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs deleted file mode 100644 index dc61aacd7..000000000 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; - -namespace Emby.Server.Implementations.Library -{ - /// <summary> - /// An invalid authentication provider. - /// </summary> - public class InvalidAuthProvider : IAuthenticationProvider - { - /// <inheritdoc /> - public string Name => "InvalidOrMissingAuthenticationProvider"; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public Task<ProviderAuthenticationResult> Authenticate(string username, string password) - { - throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); - } - - /// <inheritdoc /> - public bool HasPassword(User user) - { - return true; - } - - /// <inheritdoc /> - public Task ChangePassword(User user, string newPassword) - { - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - // Nothing here - } - - /// <inheritdoc /> - public string GetPasswordHash(User user) - { - return string.Empty; - } - - /// <inheritdoc /> - public string GetEasyPasswordHash(User user) - { - return string.Empty; - } - } -} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 50a5135d4..c27b73c74 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -17,14 +17,16 @@ using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -35,6 +37,7 @@ using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -44,19 +47,43 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Person = MediaBrowser.Controller.Entities.Person; using SortOrder = MediaBrowser.Model.Entities.SortOrder; using VideoResolver = Emby.Naming.Video.VideoResolver; namespace Emby.Server.Implementations.Library { /// <summary> - /// Class LibraryManager + /// Class LibraryManager. /// </summary> public class LibraryManager : ILibraryManager { + private readonly ILogger<LibraryManager> _logger; + private readonly ITaskManager _taskManager; + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly IServerConfigurationManager _configurationManager; + private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory; + private readonly Lazy<IProviderManager> _providerManagerFactory; + private readonly Lazy<IUserViewManager> _userviewManagerFactory; + private readonly IServerApplicationHost _appHost; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly IItemRepository _itemRepository; + private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; + private readonly IImageProcessor _imageProcessor; + private NamingOptions _namingOptions; private string[] _videoFileExtensions; + private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value; + + private IProviderManager ProviderManager => _providerManagerFactory.Value; + + private IUserViewManager UserViewManager => _userviewManagerFactory.Value; + /// <summary> /// Gets or sets the postscan tasks. /// </summary> @@ -70,13 +97,13 @@ namespace Emby.Server.Implementations.Library private IIntroProvider[] IntroProviders { get; set; } /// <summary> - /// Gets or sets the list of entity resolution ignore rules + /// Gets or sets the list of entity resolution ignore rules. /// </summary> /// <value>The entity resolution ignore rules.</value> private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// <summary> - /// Gets or sets the list of currently registered entity resolvers + /// Gets or sets the list of currently registered entity resolvers. /// </summary> /// <value>The entity resolvers enumerable.</value> private IItemResolver[] EntityResolvers { get; set; } @@ -90,12 +117,6 @@ namespace Emby.Server.Implementations.Library private IBaseItemComparer[] Comparers { get; set; } /// <summary> - /// Gets or sets the active item repository - /// </summary> - /// <value>The item repository.</value> - public IItemRepository ItemRepository { get; set; } - - /// <summary> /// Occurs when [item added]. /// </summary> public event EventHandler<ItemChangeEventArgs> ItemAdded; @@ -110,90 +131,56 @@ namespace Emby.Server.Implementations.Library /// </summary> public event EventHandler<ItemChangeEventArgs> ItemRemoved; - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// The _task manager - /// </summary> - private readonly ITaskManager _taskManager; - - /// <summary> - /// The _user manager - /// </summary> - private readonly IUserManager _userManager; - - /// <summary> - /// The _user data repository - /// </summary> - private readonly IUserDataManager _userDataRepository; - - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } - - private readonly Func<ILibraryMonitor> _libraryMonitorFactory; - private readonly Func<IProviderManager> _providerManagerFactory; - private readonly Func<IUserViewManager> _userviewManager; public bool IsScanRunning { get; private set; } - private IServerApplicationHost _appHost; - private readonly IMediaEncoder _mediaEncoder; - - /// <summary> - /// The _library items cache - /// </summary> - private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; - - /// <summary> - /// Gets the library items cache. - /// </summary> - /// <value>The library items cache.</value> - private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache => _libraryItemsCache; - - private readonly IFileSystem _fileSystem; - /// <summary> /// Initializes a new instance of the <see cref="LibraryManager" /> class. /// </summary> - /// <param name="appHost">The application host</param> - /// <param name="loggerFactory">The logger factory.</param> + /// <param name="appHost">The application host.</param> + /// <param name="logger">The logger.</param> /// <param name="taskManager">The task manager.</param> /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> + /// <param name="libraryMonitorFactory">The library monitor.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="providerManagerFactory">The provider manager.</param> + /// <param name="userviewManagerFactory">The userview manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="itemRepository">The item repository.</param> + /// <param name="imageProcessor">The image processor.</param> public LibraryManager( IServerApplicationHost appHost, - ILoggerFactory loggerFactory, + ILogger<LibraryManager> logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, - Func<ILibraryMonitor> libraryMonitorFactory, + Lazy<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, - Func<IProviderManager> providerManagerFactory, - Func<IUserViewManager> userviewManager, - IMediaEncoder mediaEncoder) + Lazy<IProviderManager> providerManagerFactory, + Lazy<IUserViewManager> userviewManagerFactory, + IMediaEncoder mediaEncoder, + IItemRepository itemRepository, + IImageProcessor imageProcessor) { _appHost = appHost; - _logger = loggerFactory.CreateLogger(nameof(LibraryManager)); + _logger = logger; _taskManager = taskManager; _userManager = userManager; - ConfigurationManager = configurationManager; + _configurationManager = configurationManager; _userDataRepository = userDataRepository; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; - _userviewManager = userviewManager; + _userviewManagerFactory = userviewManagerFactory; _mediaEncoder = mediaEncoder; + _itemRepository = itemRepository; + _imageProcessor = imageProcessor; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); - ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; + _configurationManager.ConfigurationUpdated += ConfigurationUpdated; RecordConfigurationValues(configurationManager.Configuration); } @@ -222,12 +209,12 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// The _root folder + /// The _root folder. /// </summary> private volatile AggregateFolder _rootFolder; /// <summary> - /// The _root folder sync lock + /// The _root folder sync lock. /// </summary> private readonly object _rootFolderSyncLock = new object(); @@ -272,7 +259,7 @@ namespace Emby.Server.Implementations.Library /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> private void ConfigurationUpdated(object sender, EventArgs e) { - var config = ConfigurationManager.Configuration; + var config = _configurationManager.Configuration; var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted; @@ -306,7 +293,7 @@ namespace Emby.Server.Implementations.Library } } - LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); + _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -354,7 +341,7 @@ namespace Emby.Server.Implementations.Library if (item is LiveTvProgram) { _logger.LogDebug( - "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -363,7 +350,7 @@ namespace Emby.Server.Implementations.Library else { _logger.LogInformation( - "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -381,7 +368,12 @@ namespace Emby.Server.Implementations.Library continue; } - _logger.LogDebug("Deleting path {MetadataPath}", metadataPath); + _logger.LogDebug( + "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); try { @@ -405,7 +397,13 @@ namespace Emby.Server.Implementations.Library { try { - _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); + _logger.LogInformation( + "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + if (fileSystemInfo.IsDirectory) { Directory.Delete(fileSystemInfo.FullName, true); @@ -437,10 +435,10 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - ItemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem(item.Id); foreach (var child in children) { - ItemRepository.DeleteItem(child.Id); + _itemRepository.DeleteItem(child.Id); } _libraryItemsCache.TryRemove(item.Id, out BaseItem removed); @@ -509,15 +507,15 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(type)); } - if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) + 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) + key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) .TrimStart(new[] { '/', '\\' }) .Replace("/", "\\"); } - if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) + if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) { key = key.ToLowerInvariant(); } @@ -550,7 +548,7 @@ namespace Emby.Server.Implementations.Library collectionType = GetContentTypeOverride(fullPath, true); } - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) { Parent = parent, Path = fullPath, @@ -639,7 +637,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Determines whether a path should be ignored based on its contents - called after the contents have been read + /// Determines whether a path should be ignored based on its contents - called after the contents have been read. /// </summary> /// <param name="args">The args.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> @@ -720,11 +718,13 @@ namespace Emby.Server.Implementations.Library /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception> public AggregateFolder CreateRootFolder() { - var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; + var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath; Directory.CreateDirectory(rootFolderPath); - var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))).DeepCopy<Folder, AggregateFolder>(); + var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? + ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) + .DeepCopy<Folder, AggregateFolder>(); // In case program data folder was moved if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal)) @@ -734,7 +734,7 @@ namespace Emby.Server.Implementations.Library } // Add in the plug-in folders - var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists"); + var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists"); Directory.CreateDirectory(path); @@ -786,7 +786,7 @@ namespace Emby.Server.Implementations.Library { if (_userRootFolder == null) { - var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; _logger.LogDebug("Creating userRootPath at {path}", userRootPath); Directory.CreateDirectory(userRootPath); @@ -919,7 +919,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets a Genre + /// Gets a Genre. /// </summary> /// <param name="name">The name.</param> /// <returns>Task{Genre}.</returns> @@ -980,7 +980,7 @@ namespace Emby.Server.Implementations.Library where T : BaseItem, new() { var path = getPathFn(name); - var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds; + var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds; return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); } @@ -994,13 +994,13 @@ namespace Emby.Server.Implementations.Library public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) { // Ensure the location is available. - Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); + Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath); return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress); } /// <summary> - /// Reloads the root media folder + /// Reloads the root media folder. /// </summary> /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -1031,7 +1031,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken) { IsScanRunning = true; - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -1039,7 +1039,7 @@ namespace Emby.Server.Implementations.Library } finally { - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); IsScanRunning = false; } } @@ -1148,7 +1148,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - ItemRepository.UpdateInheritedValues(cancellationToken); + _itemRepository.UpdateInheritedValues(cancellationToken); progress.Report(100); } @@ -1168,9 +1168,9 @@ namespace Emby.Server.Implementations.Library var topLibraryFolders = GetUserRootFolder().Children.ToList(); _logger.LogDebug("Getting refreshQueue"); - var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null; + var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null; - return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath) + return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath) .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue)) .ToList(); } @@ -1245,7 +1245,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (LibraryItemsCache.TryGetValue(id, out BaseItem item)) + if (_libraryItemsCache.TryGetValue(id, out BaseItem item)) { return item; } @@ -1276,7 +1276,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User, allowExternalContent); } - return ItemRepository.GetItemList(query); + return _itemRepository.GetItemList(query); } public List<BaseItem> GetItemList(InternalItemsQuery query) @@ -1300,7 +1300,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return ItemRepository.GetCount(query); + return _itemRepository.GetCount(query); } public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) @@ -1315,7 +1315,7 @@ namespace Emby.Server.Implementations.Library } } - return ItemRepository.GetItemList(query); + return _itemRepository.GetItemList(query); } public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) @@ -1327,12 +1327,12 @@ namespace Emby.Server.Implementations.Library if (query.EnableTotalRecordCount) { - return ItemRepository.GetItems(query); + return _itemRepository.GetItems(query); } return new QueryResult<BaseItem> { - Items = ItemRepository.GetItemList(query).ToArray() + Items = _itemRepository.GetItemList(query).ToArray() }; } @@ -1343,7 +1343,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return ItemRepository.GetItemIdsList(query); + return _itemRepository.GetItemIdsList(query); } public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query) @@ -1354,7 +1354,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetStudios(query); + return _itemRepository.GetStudios(query); } public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query) @@ -1365,7 +1365,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetGenres(query); + return _itemRepository.GetGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query) @@ -1376,7 +1376,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetMusicGenres(query); + return _itemRepository.GetMusicGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query) @@ -1387,7 +1387,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetAllArtists(query); + return _itemRepository.GetAllArtists(query); } public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query) @@ -1398,7 +1398,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetArtists(query); + return _itemRepository.GetArtists(query); } private void SetTopParentOrAncestorIds(InternalItemsQuery query) @@ -1439,7 +1439,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetAlbumArtists(query); + return _itemRepository.GetAlbumArtists(query); } public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) @@ -1460,10 +1460,10 @@ namespace Emby.Server.Implementations.Library if (query.EnableTotalRecordCount) { - return ItemRepository.GetItems(query); + return _itemRepository.GetItems(query); } - var list = ItemRepository.GetItemList(query); + var list = _itemRepository.GetItemList(query); return new QueryResult<BaseItem> { @@ -1509,7 +1509,7 @@ namespace Emby.Server.Implementations.Library string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && query.ItemIds.Length == 0) { - var userViews = _userviewManager().GetUserViews(new UserViewQuery + var userViews = UserViewManager.GetUserViews(new UserViewQuery { UserId = user.Id, IncludeHidden = true, @@ -1553,7 +1553,8 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) + if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) + && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() .GetChildren(user, true) @@ -1802,14 +1803,14 @@ namespace Emby.Server.Implementations.Library /// Creates the items. /// </summary> /// <param name="items">The items.</param> - /// <param name="parent">The parent item</param> + /// <param name="parent">The parent item.</param> /// <param name="cancellationToken">The cancellation token.</param> public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken) { // Don't iterate multiple times var itemsList = items.ToList(); - ItemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(itemsList, cancellationToken); foreach (var item in itemsList) { @@ -1844,10 +1845,100 @@ namespace Emby.Server.Implementations.Library } } - public void UpdateImages(BaseItem item) + private bool ImageNeedsRefresh(ItemImageInfo image) { - ItemRepository.SaveImages(item); + if (image.Path != null && image.IsLocalFile) + { + if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash)) + { + return true; + } + try + { + return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot get file info for {0}", image.Path); + return false; + } + } + + return image.Path != null && !image.IsLocalFile; + } + + public void UpdateImages(BaseItem item, bool forceUpdate = false) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); + if (outdated.Length == 0) + { + RegisterItem(item); + return; + } + + foreach (var img in outdated) + { + var image = img; + if (!img.IsLocalFile) + { + try + { + var index = item.GetImageIndex(img); + image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (ArgumentException) + { + _logger.LogWarning("Cannot get image index for {0}", img.Path); + continue; + } + catch (InvalidOperationException) + { + _logger.LogWarning("Cannot fetch image from {0}", img.Path); + continue; + } + } + + try + { + ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); + image.Width = size.Width; + image.Height = size.Height; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path); + image.Width = 0; + image.Height = 0; + continue; + } + + try + { + image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path); + image.BlurHash = string.Empty; + } + + try + { + image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path); + } + } + + _itemRepository.SaveImages(item); RegisterItem(item); } @@ -1863,15 +1954,15 @@ namespace Emby.Server.Implementations.Library { if (item.IsFileProtocol) { - _providerManagerFactory().SaveMetadata(item, updateReason); + ProviderManager.SaveMetadata(item, updateReason); } item.DateLastSaved = DateTime.UtcNow; - RegisterItem(item); + UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); } - ItemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(itemsList, cancellationToken); if (ItemUpdated != null) { @@ -1947,7 +2038,7 @@ namespace Emby.Server.Implementations.Library /// <returns>BaseItem.</returns> public BaseItem RetrieveItem(Guid id) { - return ItemRepository.RetrieveItem(id); + return _itemRepository.RetrieveItem(id); } public List<Folder> GetCollectionFolders(BaseItem item) @@ -2066,7 +2157,7 @@ namespace Emby.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes + var nameValuePair = _configurationManager.Configuration.ContentTypes .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); @@ -2115,7 +2206,7 @@ namespace Emby.Server.Implementations.Library string sortName) { var path = Path.Combine( - ConfigurationManager.ApplicationPaths.InternalMetadataPath, + _configurationManager.ApplicationPaths.InternalMetadataPath, "views", _fileSystem.GetValidFilename(viewType)); @@ -2147,7 +2238,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); + ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; @@ -2165,7 +2256,7 @@ namespace Emby.Server.Implementations.Library var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); + var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; @@ -2202,7 +2293,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2269,7 +2360,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2303,7 +2394,7 @@ namespace Emby.Server.Implementations.Library var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); + var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; @@ -2346,7 +2437,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2524,7 +2615,7 @@ namespace Emby.Server.Implementations.Library Anime series don't generally have a season in their file name, however, tvdb needs a season to correctly get the metadata. Hence, a null season needs to be filled with something. */ - //FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified + // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified episode.ParentIndexNumber = 1; } @@ -2675,8 +2766,8 @@ namespace Emby.Server.Implementations.Library } } - var metadataPath = ConfigurationManager.Configuration.MetadataPath; - var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath; + var metadataPath = _configurationManager.Configuration.MetadataPath; + var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath)) { @@ -2687,7 +2778,7 @@ namespace Emby.Server.Implementations.Library } } - foreach (var map in ConfigurationManager.Configuration.PathSubstitutions) + foreach (var map in _configurationManager.Configuration.PathSubstitutions) { if (!string.IsNullOrWhiteSpace(map.From)) { @@ -2713,10 +2804,12 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(path)); } + if (string.IsNullOrWhiteSpace(from)) { throw new ArgumentNullException(nameof(from)); } + if (string.IsNullOrWhiteSpace(to)) { throw new ArgumentNullException(nameof(to)); @@ -2756,7 +2849,7 @@ namespace Emby.Server.Implementations.Library public List<PersonInfo> GetPeople(InternalPeopleQuery query) { - return ItemRepository.GetPeople(query); + return _itemRepository.GetPeople(query); } public List<PersonInfo> GetPeople(BaseItem item) @@ -2779,7 +2872,7 @@ namespace Emby.Server.Implementations.Library public List<Person> GetPeopleItems(InternalPeopleQuery query) { - return ItemRepository.GetPeopleNames(query).Select(i => + return _itemRepository.GetPeopleNames(query).Select(i => { try { @@ -2790,13 +2883,12 @@ namespace Emby.Server.Implementations.Library _logger.LogError(ex, "Error getting person"); return null; } - }).Where(i => i != null).ToList(); } public List<string> GetPeopleNames(InternalPeopleQuery query) { - return ItemRepository.GetPeopleNames(query); + return _itemRepository.GetPeopleNames(query); } public void UpdatePeople(BaseItem item, List<PersonInfo> people) @@ -2806,7 +2898,7 @@ namespace Emby.Server.Implementations.Library return; } - ItemRepository.UpdatePeople(item.Id, people); + _itemRepository.UpdatePeople(item.Id, people); } public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) @@ -2817,7 +2909,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url); - await _providerManagerFactory().SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); + await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); @@ -2825,7 +2917,8 @@ namespace Emby.Server.Implementations.Library } catch (HttpException ex) { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + if (ex.StatusCode.HasValue + && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden)) { continue; } @@ -2850,7 +2943,7 @@ namespace Emby.Server.Implementations.Library name = _fileSystem.GetValidFilename(name); - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, name); while (Directory.Exists(virtualFolderPath)) @@ -2869,7 +2962,7 @@ namespace Emby.Server.Implementations.Library } } - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -2904,7 +2997,7 @@ namespace Emby.Server.Implementations.Library { // Need to add a delay here or directory watchers may still pick up the changes await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); } } } @@ -2918,21 +3011,6 @@ namespace Emby.Server.Implementations.Library }); } - private static bool ValidateNetworkPath(string path) - { - //if (Environment.OSVersion.Platform == PlatformID.Win32NT) - //{ - // // We can't validate protocol-based paths, so just allow them - // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1) - // { - // return Directory.Exists(path); - // } - //} - - // Without native support for unc, we cannot validate this when running under mono - return true; - } - private const string ShortcutFileExtension = ".mblink"; public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) @@ -2959,12 +3037,7 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The path does not exist."); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) - { - throw new FileNotFoundException("The network path does not exist."); - } - - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var shortcutFilename = Path.GetFileNameWithoutExtension(path); @@ -3002,12 +3075,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(pathInfo)); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) - { - throw new FileNotFoundException("The network path does not exist."); - } - - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); @@ -3060,7 +3128,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var path = Path.Combine(rootFolderPath, name); @@ -3069,7 +3137,7 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The media folder does not exist"); } - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -3089,7 +3157,7 @@ namespace Emby.Server.Implementations.Library { // Need to add a delay here or directory watchers may still pick up the changes await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); } } } @@ -3103,7 +3171,7 @@ namespace Emby.Server.Implementations.Library var removeList = new List<NameValuePair>(); - foreach (var contentType in ConfigurationManager.Configuration.ContentTypes) + foreach (var contentType in _configurationManager.Configuration.ContentTypes) { if (string.IsNullOrWhiteSpace(contentType.Name)) { @@ -3118,11 +3186,11 @@ namespace Emby.Server.Implementations.Library if (removeList.Count > 0) { - ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes + _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes .Except(removeList) - .ToArray(); + .ToArray(); - ConfigurationManager.SaveConfiguration(); + _configurationManager.SaveConfiguration(); } } @@ -3133,7 +3201,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(mediaPath)); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); if (!Directory.Exists(virtualFolderPath)) diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index ed7d8aa40..9b9f53049 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Library { mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath); - //_logger.LogDebug("Found cached media info"); + // _logger.LogDebug("Found cached media info"); } catch { @@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Library Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); _json.SerializeToFile(mediaInfo, cacheFilePath); - //_logger.LogDebug("Saved media info to {0}", cacheFilePath); + // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } } @@ -148,17 +148,14 @@ namespace Emby.Server.Implementations.Library { videoStream.BitRate = 30000000; } - else if (width >= 1900) { videoStream.BitRate = 20000000; } - else if (width >= 1200) { videoStream.BitRate = 8000000; } - else if (width >= 700) { videoStream.BitRate = 2000000; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 70d5bd9f4..ceb36b389 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -7,6 +7,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; @@ -14,7 +16,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -33,13 +34,13 @@ namespace Emby.Server.Implementations.Library private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; + private readonly ILogger<MediaSourceManager> _logger; + private readonly IUserDataManager _userDataManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly ILocalizationManager _localizationManager; + private readonly IApplicationPaths _appPaths; private IMediaSourceProvider[] _providers; - private readonly ILogger _logger; - private readonly IUserDataManager _userDataManager; - private readonly Func<IMediaEncoder> _mediaEncoder; - private ILocalizationManager _localizationManager; - private IApplicationPaths _appPaths; public MediaSourceManager( IItemRepository itemRepo, @@ -47,16 +48,16 @@ namespace Emby.Server.Implementations.Library ILocalizationManager localizationManager, IUserManager userManager, ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger<MediaSourceManager> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, - Func<IMediaEncoder> mediaEncoder) + IMediaEncoder mediaEncoder) { _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(MediaSourceManager)); + _logger = logger; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _userDataManager = userDataManager; @@ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - if (!user.Policy.EnableAudioPlaybackTranscoding) - { - source.SupportsTranscoding = false; - } + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } } } @@ -207,22 +205,27 @@ namespace Emby.Server.Implementations.Library { return MediaProtocol.Rtsp; } + if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Rtmp; } + if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Http; } + if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Rtp; } + if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Ftp; } + if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Udp; @@ -352,7 +355,9 @@ namespace Emby.Server.Implementations.Library private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) + if (userData.SubtitleStreamIndex.HasValue + && user.RememberSubtitleSelections + && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -363,26 +368,27 @@ namespace Emby.Server.Implementations.Library } } - var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) - ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference); + + var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) + ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); - source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, + source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex( + source.MediaStreams, preferredSubs, - user.Configuration.SubtitleMode, + user.SubtitleMode, audioLangage); - MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, - user.Configuration.SubtitleMode, audioLangage); + MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); } private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection) + if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection) { var index = userData.AudioStreamIndex.Value; // Make sure the saved index is still valid @@ -393,11 +399,11 @@ namespace Emby.Server.Implementations.Library } } - var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) + var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) ? Array.Empty<string>() - : NormalizeLanguage(user.Configuration.AudioLanguagePreference); + : NormalizeLanguage(user.AudioLanguagePreference); - source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); + source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) @@ -435,7 +441,6 @@ namespace Emby.Server.Implementations.Library } return 1; - }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { @@ -496,7 +501,7 @@ namespace Emby.Server.Implementations.Library // hack - these two values were taken from LiveTVMediaSourceProvider string cacheKey = request.OpenToken; - await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths) + await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths) .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) .ConfigureAwait(false); } @@ -521,11 +526,7 @@ namespace Emby.Server.Implementations.Library SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); } - return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse - { - MediaSource = clone - - }, liveStream as IDirectStreamProvider); + return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); } private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) @@ -538,7 +539,7 @@ namespace Emby.Server.Implementations.Library mediaSource.RunTimeTicks = null; } - var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); if (audioStream == null || audioStream.Index == -1) { @@ -549,7 +550,7 @@ namespace Emby.Server.Implementations.Library mediaSource.DefaultAudioStreamIndex = audioStream.Index; } - var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); if (videoStream != null) { if (!videoStream.BitRate.HasValue) @@ -560,17 +561,14 @@ namespace Emby.Server.Implementations.Library { videoStream.BitRate = 30000000; } - else if (width >= 1900) { videoStream.BitRate = 20000000; } - else if (width >= 1200) { videoStream.BitRate = 8000000; } - else if (width >= 700) { videoStream.BitRate = 2000000; @@ -621,12 +619,11 @@ namespace Emby.Server.Implementations.Library if (liveStreamInfo is IDirectStreamProvider) { - var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest { MediaSource = mediaSource, ExtractChapters = false, MediaType = DlnaProfileType.Video - }, cancellationToken).ConfigureAwait(false); mediaSource.MediaStreams = info.MediaStreams; @@ -652,7 +649,7 @@ namespace Emby.Server.Implementations.Library { mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath); - //_logger.LogDebug("Found cached media info"); + // _logger.LogDebug("Found cached media info"); } catch (Exception ex) { @@ -674,20 +671,21 @@ namespace Emby.Server.Implementations.Library mediaSource.AnalyzeDurationMs = 3000; } - mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + mediaInfo = await _mediaEncoder.GetMediaInfo( + new MediaInfoRequest { MediaSource = mediaSource, MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, ExtractChapters = false - - }, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); if (cacheFilePath != null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath); - //_logger.LogDebug("Saved media info to {0}", cacheFilePath); + // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } } @@ -753,17 +751,14 @@ namespace Emby.Server.Implementations.Library { videoStream.BitRate = 30000000; } - else if (width >= 1900) { videoStream.BitRate = 20000000; } - else if (width >= 1200) { videoStream.BitRate = 8000000; } - else if (width >= 700) { videoStream.BitRate = 2000000; diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6b9f4d052..ca904c4ec 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MediaBrowser.Model.Configuration; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library @@ -35,7 +35,8 @@ namespace Emby.Server.Implementations.Library return null; } - public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams, + public static int? GetDefaultSubtitleStreamIndex( + List<MediaStream> streams, string[] preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) @@ -115,7 +116,8 @@ namespace Emby.Server.Implementations.Library .ThenBy(i => i.Index); } - public static void SetSubtitleStreamScores(List<MediaStream> streams, + public static void SetSubtitleStreamScores( + List<MediaStream> streams, string[] preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 1ec578371..0bdc59914 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -10,6 +11,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; namespace Emby.Server.Implementations.Library { @@ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library { return Guid.Empty; } - }).Where(i => !i.Equals(Guid.Empty)).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); @@ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } - var playlist = item as Playlist; - if (playlist != null) + if (item is Playlist playlist) { return GetInstantMixFromPlaylist(playlist, user, dtoOptions); } - var album = item as MusicAlbum; - if (album != null) + if (item is MusicAlbum album) { return GetInstantMixFromAlbum(album, user, dtoOptions); } - var artist = item as MusicArtist; - if (artist != null) + if (item is MusicArtist artist) { return GetInstantMixFromArtist(artist, user, dtoOptions); } - var song = item as Audio; - if (song != null) + if (item is Audio song) { return GetInstantMixFromSong(song, user, dtoOptions); } - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { return GetInstantMixFromFolder(folder, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 1d61ed57e..06ff3e611 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Text.RegularExpressions; @@ -12,24 +14,24 @@ namespace Emby.Server.Implementations.Library /// Gets the attribute value. /// </summary> /// <param name="str">The STR.</param> - /// <param name="attrib">The attrib.</param> + /// <param name="attribute">The attrib.</param> /// <returns>System.String.</returns> - /// <exception cref="ArgumentNullException">attrib</exception> - public static string GetAttributeValue(this string str, string attrib) + /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception> + public static string? GetAttributeValue(this string str, string attribute) { - if (string.IsNullOrEmpty(str)) + if (str.Length == 0) { - throw new ArgumentNullException(nameof(str)); + throw new ArgumentException("String can't be empty.", nameof(str)); } - if (string.IsNullOrEmpty(attrib)) + if (attribute.Length == 0) { - throw new ArgumentNullException(nameof(attrib)); + throw new ArgumentException("String can't be empty.", nameof(attribute)); } - string srch = "[" + attrib + "="; + string srch = "[" + attribute + "="; int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - if (start > -1) + if (start != -1) { start += srch.Length; int end = str.IndexOf(']', start); @@ -37,7 +39,7 @@ namespace Emby.Server.Implementations.Library } // for imdbid we also accept pattern matching - if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) { var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 34dcbbe28..4e4cac75b 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Ensures DateCreated and DateModified have values + /// Ensures DateCreated and DateModified have values. /// </summary> /// <param name="fileSystem">The file system.</param> /// <param name="item">The item.</param> @@ -118,10 +118,12 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(fileSystem)); } + if (item == null) { throw new ArgumentNullException(nameof(item)); } + if (args == null) { throw new ArgumentNullException(nameof(args)); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index fefc8e789..03059e6d3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -209,8 +209,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio Name = parseName ? resolvedItem.Name : Path.GetFileNameWithoutExtension(firstMedia.Path), - //AdditionalParts = resolvedItem.Files.Skip(1).Select(i => i.Path).ToArray(), - //LocalAlternateVersions = resolvedItem.AlternateVersions.Select(i => i.Path).ToArray() + // AdditionalParts = resolvedItem.Files.Skip(1).Select(i => i.Path).ToArray(), + // LocalAlternateVersions = resolvedItem.AlternateVersions.Select(i => i.Path).ToArray() }; result.Items.Add(libraryItem); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 85b1b6e32..79b6dded3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicAlbumResolver : ItemResolver<MusicAlbum> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <param name="logger">The logger.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> - public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _fileSystem = fileSystem; @@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio // Args points to an album if parent is an Artist folder or it directly contains music if (args.IsDirectory) { - // if (args.Parent is MusicArtist) return true; //saves us from testing children twice + // if (args.Parent is MusicArtist) return true; // saves us from testing children twice if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager)) { return true; @@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio IEnumerable<FileSystemMetadata> list, bool allowSubfolders, IDirectoryService directoryService, - ILogger logger, + ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 681db4896..5f5cd0e92 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicArtistResolver : ItemResolver<MusicArtist> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; @@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> - /// <param name="logger">The logger.</param> + /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The configuration manager.</param> public MusicArtistResolver( - ILogger<MusicArtistResolver> logger, + ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index fb75593bd..2f5e46038 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -292,7 +292,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } return true; - //var blurayExtensions = new[] + // var blurayExtensions = new[] //{ // ".mts", // ".m2ts", @@ -300,7 +300,7 @@ namespace Emby.Server.Implementations.Library.Resolvers // ".mpls" //}; - //return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)); + // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 0b93ebeb8..86a5d8b7d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book> { - private readonly string[] _validExtensions = { ".pdf", ".epub", ".mobi", ".cbr", ".cbz", ".azw3" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; protected override Book Resolve(ItemResolveArgs args) { @@ -19,7 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books // Only process items that are in a collection folder containing books if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) + { return null; + } if (args.IsDirectory) { @@ -55,7 +57,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books // Don't return a Book if there is more (or less) than one document in the directory if (bookFiles.Count != 1) + { return null; + } return new Book { diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 32ccc7fdd..9ca76095b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers public virtual ResolverPriority Priority => ResolverPriority.First; /// <summary> - /// Sets initial values on the newly resolved item + /// Sets initial values on the newly resolved item. /// </summary> /// <param name="item">The item.</param> /// <param name="args">The args.</param> diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index e4bc4a469..295e9e120 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrEmpty(id)) { - item.SetProviderId(MetadataProviders.Tmdb, id); + item.SetProviderId(MetadataProvider.Tmdb, id); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index cb67c8aa7..baf0e3cf9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -350,7 +350,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrWhiteSpace(tmdbid)) { - item.SetProviderId(MetadataProviders.Tmdb, tmdbid); + item.SetProviderId(MetadataProvider.Tmdb, tmdbid); } } @@ -361,7 +361,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrWhiteSpace(imdbid)) { - item.SetProviderId(MetadataProviders.Imdb, imdbid); + item.SetProviderId(MetadataProvider.Imdb, imdbid); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 1030ed39d..99f304190 100644 --- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -41,10 +41,12 @@ namespace Emby.Server.Implementations.Library.Resolvers { return new AggregateFolder(); } + if (string.Equals(args.Path, _appPaths.DefaultUserViewsPath, StringComparison.OrdinalIgnoreCase)) { - return new UserRootFolder(); //if we got here and still a root - must be user root + return new UserRootFolder(); // if we got here and still a root - must be user root } + if (args.IsVf) { return new CollectionFolder @@ -73,7 +75,6 @@ namespace Emby.Server.Implementations.Library.Resolvers { return false; } - }) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) .FirstOrDefault(); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 7f477a0f0..2f7af60c0 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -55,6 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV episode.SeriesId = series.Id; episode.SeriesName = series.Name; } + if (season != null) { episode.SeasonId = season.Id; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 18145b7f1..c8e41001a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -16,15 +16,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV private readonly IServerConfigurationManager _config; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; - private readonly ILogger _logger; + private readonly ILogger<SeasonResolver> _logger; /// <summary> /// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// </summary> /// <param name="config">The config.</param> /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization</param> - /// <param name="logger">The logger</param> + /// <param name="localization">The localization.</param> + /// <param name="logger">The logger.</param> public SeasonResolver( IServerConfigurationManager config, ILibraryManager libraryManager, @@ -94,7 +94,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV _localization.GetLocalizedString("NameSeasonNumber"), seasonNumber, args.GetLibraryOptions().PreferredMetadataLanguage); - } return season; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index dd6bd8ee8..732bfd94d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV public class SeriesResolver : FolderResolver<Series> { private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger<SeriesResolver> _logger; private readonly ILibraryManager _libraryManager; /// <summary> @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var collectionType = args.GetCollectionType(); if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - //if (args.ContainsFileSystemEntryByName("tvshow.nfo")) + // if (args.ContainsFileSystemEntryByName("tvshow.nfo")) //{ // return new Series // { @@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService, IFileSystem fileSystem, - ILogger logger, + ILogger<SeriesResolver> logger, ILibraryManager libraryManager, bool isTvContentType) { @@ -217,7 +217,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV if (!string.IsNullOrEmpty(id)) { - item.SetProviderId(MetadataProviders.Tvdb, id); + item.SetProviderId(MetadataProvider.Tvdb, id); } } } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 11d6c737a..3df9cc06f 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -12,21 +13,22 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Search; using Microsoft.Extensions.Logging; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Person = MediaBrowser.Controller.Entities.Person; namespace Emby.Server.Implementations.Library { public class SearchEngine : ISearchEngine { + private readonly ILogger<SearchEngine> _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; - public SearchEngine(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IUserManager userManager) + public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager) { + _logger = logger; _libraryManager = libraryManager; _userManager = userManager; - - _logger = loggerFactory.CreateLogger("SearchEngine"); } public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) @@ -192,6 +194,7 @@ namespace Emby.Server.Implementations.Library { searchQuery.AncestorIds = new[] { searchQuery.ParentId }; } + searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; searchQuery.IncludeItemTypes = Array.Empty<string>(); @@ -205,7 +208,6 @@ namespace Emby.Server.Implementations.Library return mediaItems.Select(i => new SearchHintInfo { Item = i - }).ToList(); } } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 071681b08..175b3af57 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Threading; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; +using Book = MediaBrowser.Controller.Entities.Book; namespace Emby.Server.Implementations.Library { @@ -26,27 +28,26 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); - private readonly ILogger _logger; + private readonly ILogger<UserDataManager> _logger; private readonly IServerConfigurationManager _config; - - private Func<IUserManager> _userManager; - - public UserDataManager(ILoggerFactory loggerFactory, IServerConfigurationManager config, Func<IUserManager> userManager) + private readonly IUserManager _userManager; + private readonly IUserDataRepository _repository; + + public UserDataManager( + ILogger<UserDataManager> logger, + IServerConfigurationManager config, + IUserManager userManager, + IUserDataRepository repository) { + _logger = logger; _config = config; - _logger = loggerFactory.CreateLogger(GetType().Name); _userManager = userManager; + _repository = repository; } - /// <summary> - /// Gets or sets the repository. - /// </summary> - /// <value>The repository.</value> - public IUserDataRepository Repository { get; set; } - public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); SaveUserData(user, item, userData, reason, cancellationToken); } @@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.Library foreach (var key in keys) { - Repository.SaveUserData(userId, key, userData, cancellationToken); + _repository.SaveUserData(userId, key, userData, cancellationToken); } var cacheKey = GetCacheKey(userId, item.Id); @@ -96,26 +97,26 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); - Repository.SaveAllUserData(user.InternalId, userData, cancellationToken); + _repository.SaveAllUserData(user.InternalId, userData, cancellationToken); } /// <summary> - /// Retrieve all user data for the given user + /// Retrieve all user data for the given user. /// </summary> /// <param name="userId"></param> /// <returns></returns> public List<UserItemData> GetAllUserData(Guid userId) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); - return Repository.GetAllUserData(user.InternalId); + return _repository.GetAllUserData(user.InternalId); } public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); return GetUserData(user, itemId, keys); } @@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.Library private UserItemData GetUserDataInternal(long internalUserId, List<string> keys) { - var userData = Repository.GetUserData(internalUserId, keys); + var userData = _repository.GetUserData(internalUserId, keys); if (userData != null) { @@ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Converts a UserItemData to a DTOUserItemData + /// Converts a UserItemData to a DTOUserItemData. /// </summary> /// <param name="data">The data.</param> /// <returns>DtoUserItemData.</returns> @@ -241,7 +242,7 @@ namespace Emby.Server.Implementations.Library { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; - if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) + if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book)) { positionTicks = 0; data.Played = playedToCompletion = true; diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs deleted file mode 100644 index 15076a194..000000000 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ /dev/null @@ -1,1116 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -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.Cryptography; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -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 -{ - /// <summary> - /// Class UserManager. - /// </summary> - public class UserManager : IUserManager - { - private readonly object _policySyncLock = new object(); - private readonly object _configSyncLock = new object(); - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// Gets the active user repository. - /// </summary> - /// <value>The user repository.</value> - private readonly IUserRepository _userRepository; - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; - - private readonly Func<IImageProcessor> _imageProcessorFactory; - private readonly Func<IDtoService> _dtoServiceFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - private readonly ICryptoProvider _cryptoProvider; - - private ConcurrentDictionary<Guid, User> _users; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - - private InvalidAuthProvider _invalidAuthProvider; - - private IPasswordResetProvider[] _passwordResetProviders; - private DefaultPasswordResetProvider _defaultPasswordResetProvider; - - public UserManager( - ILogger<UserManager> logger, - IUserRepository userRepository, - IXmlSerializer xmlSerializer, - INetworkManager networkManager, - Func<IImageProcessor> imageProcessorFactory, - Func<IDtoService> dtoServiceFactory, - IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - IFileSystem fileSystem, - ICryptoProvider cryptoProvider) - { - _logger = logger; - _userRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _cryptoProvider = cryptoProvider; - _users = null; - } - - public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; - - /// <summary> - /// Occurs when [user updated]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserUpdated; - - public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; - - public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; - - public event EventHandler<GenericEventArgs<User>> UserLockedOut; - - public event EventHandler<GenericEventArgs<User>> UserCreated; - - /// <summary> - /// Occurs when [user deleted]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserDeleted; - - /// <inheritdoc /> - public IEnumerable<User> Users => _users.Values; - - /// <inheritdoc /> - public IEnumerable<Guid> UsersIds => _users.Keys; - - /// <summary> - /// Called when [user updated]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - - /// <summary> - /// Called when [user deleted]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - - public NameIdPair[] GetAuthenticationProviders() - { - 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 NameIdPair[] GetPasswordResetProviders() - { - return _passwordResetProviders - .Where(i => i.IsEnabled) - .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = GetPasswordResetProviderId(i) - }) - .ToArray(); - } - - public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); - - _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); - - _passwordResetProviders = passwordResetProviders.ToArray(); - - _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); - } - - /// <inheritdoc /> - public User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - _users.TryGetValue(id, out User user); - return user; - } - - public User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Invalid username", nameof(name)); - } - - return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - public void Initialize() - { - LoadUsers(); - - var users = Users; - - // 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) - => IsValidUsername(i.ToString(CultureInfo.InvariantCulture)); - - 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<User> AuthenticateUser( - string username, - string password, - string hashedPassword, - string remoteEndPoint, - bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); - 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.authenticationProvider; - success = authResult.success; - } - else - { - // user is null - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - string updatedUsername = authResult.username; - success = authResult.success; - - if (success - && authenticationProvider != null - && !(authenticationProvider is DefaultAuthenticationProvider)) - { - // Trust the username returned by the authentication provider - username = updatedUsername; - - // Search the database for the user again - // the authentication provider might have created it - user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - - if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) - { - 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) - { - _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint); - throw new AuthenticationException("Invalid username or password entered."); - } - - if (user.Policy.IsDisabled) - { - _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint); - throw new AuthenticationException( - string.Format( - CultureInfo.InvariantCulture, - "The {0} account is currently disabled. Please consult with your administrator.", - user.Name)); - } - - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint); - throw new AuthenticationException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint); - throw new AuthenticationException("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); - } - - ResetInvalidLoginAttemptCount(user); - _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name); - } - else - { - IncrementInvalidLoginAttemptCount(user); - _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint); - } - - return success ? user : null; - } - -#nullable enable - - private static string GetAuthenticationProviderId(IAuthenticationProvider provider) - { - return provider.GetType().FullName; - } - - private static string GetPasswordResetProviderId(IPasswordResetProvider provider) - { - return provider.GetType().FullName; - } - - private IAuthenticationProvider GetAuthenticationProvider(User user) - { - return GetAuthenticationProviders(user)[0]; - } - - private IPasswordResetProvider GetPasswordResetProvider(User user) - { - return GetPasswordResetProviders(user)[0]; - } - - private IAuthenticationProvider[] GetAuthenticationProviders(User? user) - { - var authenticationProviderId = 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) - { - // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found - _logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user?.Name, user?.Policy.AuthenticationProviderId); - providers = new IAuthenticationProvider[] { _invalidAuthProvider }; - } - - return providers; - } - - private IPasswordResetProvider[] GetPasswordResetProviders(User? user) - { - var passwordResetProviderId = user?.Policy.PasswordResetProviderId; - - var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(passwordResetProviderId)) - { - providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - if (providers.Length == 0) - { - providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider }; - } - - return providers; - } - - private async Task<(string username, bool success)> AuthenticateWithProvider( - IAuthenticationProvider provider, - string username, - string password, - User? resolvedUser) - { - try - { - var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser - ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) - : await provider.Authenticate(username, password).ConfigureAwait(false); - - if (authenticationResult.Username != username) - { - _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); - username = authenticationResult.Username; - } - - return (username, true); - } - catch (AuthenticationException ex) - { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - - return (username, false); - } - } - - private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser( - string username, - string password, - string hashedPassword, - User? user, - string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider? authenticationProvider = null; - - foreach (var provider in GetAuthenticationProviders(user)) - { - var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - var updatedUsername = providerAuthResult.username; - success = providerAuthResult.success; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } - } - - if (!success - && _networkManager.IsInLocalNetwork(remoteEndPoint) - && user?.Configuration.EnableLocalPassword == true - && !string.IsNullOrEmpty(user.EasyPassword)) - { - // Check easy password - var passwordHash = PasswordHash.Parse(user.EasyPassword); - var hash = _cryptoProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(password), - passwordHash.Salt.ToArray()); - success = passwordHash.Hash.SequenceEqual(hash); - } - - return (authenticationProvider, username, success); - } - - private void ResetInvalidLoginAttemptCount(User user) - { - user.Policy.InvalidLoginAttemptCount = 0; - UpdateUserPolicy(user, user.Policy, false); - } - - private void IncrementInvalidLoginAttemptCount(User user) - { - int invalidLogins = ++user.Policy.InvalidLoginAttemptCount; - int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout; - if (maxInvalidLogins > 0 - && invalidLogins >= maxInvalidLogins) - { - user.Policy.IsDisabled = true; - UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user)); - _logger.LogWarning( - "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", - user.Name, - invalidLogins); - } - - UpdateUserPolicy(user, user.Policy, false); - } - - /// <summary> - /// Loads the users from the repository. - /// </summary> - private void LoadUsers() - { - var users = _userRepository.RetrieveAllUsers(); - - // There always has to be at least one user. - if (users.Count != 0) - { - _users = new ConcurrentDictionary<Guid, User>( - users.Select(x => new KeyValuePair<Guid, User>(x.Id, x))); - return; - } - - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - - _logger.LogWarning("No users, creating one with username {UserName}", defaultName); - - var name = MakeValidUsername(defaultName); - - var user = InstantiateNewUser(name); - - user.DateLastSaved = DateTime.UtcNow; - - _userRepository.CreateUser(user); - - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - - _users = new ConcurrentDictionary<Guid, User>(); - _users[user.Id] = user; - } - -#nullable restore - - public UserDto GetUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); - bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); - - bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? - 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; - } - } - - /// <summary> - /// Refreshes metadata for each user - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - foreach (var user in Users) - { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// <summary> - /// Renames the user. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="newName">The new name.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> - public async Task RenameUser(User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrWhiteSpace(newName)) - { - throw new ArgumentException("Invalid username", nameof(newName)); - } - - if (user.Name.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - if (Users.Any( - u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - newName)); - } - - await user.Rename(newName).ConfigureAwait(false); - - OnUserUpdated(user); - } - - /// <summary> - /// Updates the user. - /// </summary> - /// <param name="user">The user.</param> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.Id == Guid.Empty) - { - throw new ArgumentException("Id can't be empty.", nameof(user)); - } - - if (!_users.ContainsKey(user.Id)) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "A user '{0}' with Id {1} does not exist.", - user.Name, - user.Id), - nameof(user)); - } - - user.DateModified = DateTime.UtcNow; - user.DateLastSaved = DateTime.UtcNow; - - _userRepository.UpdateUser(user); - - OnUserUpdated(user); - } - - /// <summary> - /// Creates the user. - /// </summary> - /// <param name="name">The name.</param> - /// <returns>User.</returns> - /// <exception cref="ArgumentNullException">name</exception> - /// <exception cref="ArgumentException"></exception> - public User 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)); - } - - var user = InstantiateNewUser(name); - - _users[user.Id] = user; - - user.DateLastSaved = DateTime.UtcNow; - - _userRepository.CreateUser(user); - - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); - - return user; - } - - /// <inheritdoc /> - /// <exception cref="ArgumentNullException">The <c>user</c> is <c>null</c>.</exception> - /// <exception cref="ArgumentException">The <c>user</c> doesn't exist, or is the last administrator.</exception> - /// <exception cref="InvalidOperationException">The <c>user</c> can't be deleted; there are no other users.</exception> - public void DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (!_users.ContainsKey(user.Id)) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", - user.Name, - user.Id)); - } - - if (_users.Count == 1) - { - throw new InvalidOperationException(string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one user in the system.", - user.Name)); - } - - if (user.Policy.IsAdministrator - && Users.Count(i => i.Policy.IsAdministrator) == 1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", - user.Name), - nameof(user)); - } - - var configPath = GetConfigurationFilePath(user); - - _userRepository.DeleteUser(user); - - // Delete user config dir - lock (_configSyncLock) - lock (_policySyncLock) - { - try - { - Directory.Delete(user.ConfigurationDirectoryPath, true); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath); - } - } - - _users.TryRemove(user.Id, out _); - - OnUserDeleted(user); - } - - /// <summary> - /// Resets the password by clearing it. - /// </summary> - /// <returns>Task.</returns> - 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>(user)); - } - - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash); - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user)); - } - - /// <summary> - /// Instantiates the new user. - /// </summary> - /// <param name="name">The name.</param> - /// <returns>User.</returns> - private static User InstantiateNewUser(string name) - { - return new User - { - Name = name, - Id = Guid.NewGuid(), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow - }; - } - - public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - var user = string.IsNullOrWhiteSpace(enteredUsername) ? - null : - GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - - if (user != null && isInNetwork) - { - var passwordResetProvider = GetPasswordResetProvider(user); - return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); - } - else - { - return new ForgotPasswordResult - { - Action = action, - PinFile = string.Empty - }; - } - } - - public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) - { - foreach (var provider in _passwordResetProviders) - { - var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); - if (result.Success) - { - return result; - } - } - - return new PinRedeemResult - { - Success = false, - UsersReset = Array.Empty<string>() - }; - } - - public UserPolicy GetUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - if (!File.Exists(path)) - { - return GetDefaultPolicy(); - } - - try - { - lock (_policySyncLock) - { - return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {Path}", path); - - return GetDefaultPolicy(); - } - } - - private static UserPolicy GetDefaultPolicy() - { - return new UserPolicy - { - EnableContentDownloading = true, - EnableSyncTranscoding = true - }; - } - - 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<UserPolicy>(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<User> { Argument = user }); - } - } - - 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 (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {Path}", path); - - return new UserConfiguration(); - } - } - - 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<UserConfiguration>(json); - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { 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<User> 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/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 322819b05..f51657c63 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -17,6 +19,8 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; +using Genre = MediaBrowser.Controller.Entities.Genre; +using Person = MediaBrowser.Controller.Entities.Person; namespace Emby.Server.Implementations.Library { @@ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library if (!query.IncludeHidden) { - list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); + list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); } var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); - var orders = user.Configuration.OrderedViews.ToList(); + var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList(); return list .OrderBy(i => @@ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library return GetUserSubViewWithName(name, parentId, type, sortName); } - private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews) + private Folder GetUserView( + List<ICollectionFolder> parents, + string viewType, + string localizationKey, + string sortName, + Jellyfin.Data.Entities.User user, + string[] presetViews) { if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) { @@ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library { parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) + .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) + .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .ToList(); } @@ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] { - typeof(Person).Name, - typeof(Studio).Name, - typeof(Year).Name, - typeof(MusicGenre).Name, - typeof(Genre).Name - + nameof(Person), + nameof(Studio), + nameof(Year), + nameof(MusicGenre), + nameof(Genre) } : Array.Empty<string>(); var query = new InternalItemsQuery(user) diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index 2af8ff5cb..d51f9aaa7 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<ArtistsValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="itemRepo">The item repository.</param> public ArtistsPostScanTask( ILibraryManager libraryManager, - ILogger<ArtistsPostScanTask> logger, + ILogger<ArtistsValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 1497f4a3a..d4c8c35e6 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <summary> /// The logger. /// </summary> - private readonly ILogger _logger; + private readonly ILogger<ArtistsValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; @@ -98,7 +98,6 @@ namespace Emby.Server.Implementations.Library.Validators _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false - }, false); } diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index 251785dfd..d21d2887b 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<GenresValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="itemRepo">The item repository.</param> public GenresPostScanTask( ILibraryManager libraryManager, - ILogger<GenresPostScanTask> logger, + ILogger<GenresValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index b0cd5f87a..e59c62e23 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <summary> /// The logger. /// </summary> - private readonly ILogger _logger; + private readonly ILogger<GenresValidator> _logger; /// <summary> /// Initializes a new instance of the <see cref="GenresValidator"/> class. @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index 9d8690116..be119866b 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// </summary> private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<MusicGenresValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="itemRepo">The item repository.</param> public MusicGenresPostScanTask( ILibraryManager libraryManager, - ILogger<MusicGenresPostScanTask> logger, + ILogger<MusicGenresValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 5ee4ca72e..1ecf4c87c 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <summary> /// The logger. /// </summary> - private readonly ILogger _logger; + private readonly ILogger<MusicGenresValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index 2f8f906b9..c682b156b 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Library.Validators /// </summary> private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<StudiosValidator> _logger; private readonly IItemRepository _itemRepo; /// <summary> @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="itemRepo">The item repository.</param> public StudiosPostScanTask( ILibraryManager libraryManager, - ILogger<StudiosPostScanTask> logger, + ILogger<StudiosValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 15e7a0dbb..ca35adfff 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <summary> /// The logger. /// </summary> - private readonly ILogger _logger; + private readonly ILogger<StudiosValidator> _logger; /// <summary> /// Initializes a new instance of the <see cref="StudiosValidator" /> class. @@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Validators /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repository.</param> - public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; @@ -92,7 +92,6 @@ namespace Emby.Server.Implementations.Library.Validators _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false - }, false); } |
