diff options
Diffstat (limited to 'Emby.Server.Implementations/Library')
26 files changed, 1994 insertions, 1048 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 5c3e1dab1..59f0a9fc9 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -22,10 +22,12 @@ namespace Emby.Server.Implementations.Library private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; + private bool _ignoreDotPrefix; + /// <summary> /// Any folder named in this list will be ignored - can be added to at runtime for extensibility /// </summary> - public static readonly List<string> IgnoreFolders = new List<string> + public static readonly Dictionary<string, string> IgnoreFolders = new List<string> { "metadata", "ps3_update", @@ -41,15 +43,24 @@ namespace Emby.Server.Implementations.Library "#recycle", // Qnap - "@Recycle" + "@Recycle", + ".@__thumb", + "$RECYCLE.BIN", + "System Volume Information", + ".grab", + + // macos + ".AppleDouble" + + }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - }; - public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _fileSystem = fileSystem; _libraryManager = libraryManager; _logger = logger; + + _ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT; } /// <summary> @@ -67,46 +78,48 @@ namespace Emby.Server.Implementations.Library } var filename = fileInfo.Name; - var isHidden = fileInfo.IsHidden; var path = fileInfo.FullName; // Handle mac .DS_Store // https://github.com/MediaBrowser/MediaBrowser/issues/427 - if (filename.IndexOf("._", StringComparison.OrdinalIgnoreCase) == 0) + if (_ignoreDotPrefix) { - return true; - } - - // Ignore hidden files and folders - if (isHidden) - { - if (parent == null) + if (filename.IndexOf('.') == 0) { - var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path)); - - if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - // Sometimes these are marked hidden - if (_fileSystem.IsRootPath(path)) - { - return false; + return true; } - - return true; } + // Ignore hidden files and folders + //if (fileInfo.IsHidden) + //{ + // if (parent == null) + // { + // var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path)); + + // if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + // { + // return false; + // } + // if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + // { + // return false; + // } + // } + + // // Sometimes these are marked hidden + // if (_fileSystem.IsRootPath(path)) + // { + // return false; + // } + + // return true; + //} + if (fileInfo.IsDirectory) { // Ignore any folders in our list - if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) + if (IgnoreFolders.ContainsKey(filename)) { return true; } @@ -141,6 +154,17 @@ namespace Emby.Server.Implementations.Library return true; } } + + // Ignore samples + var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) + .Replace("-", " ", StringComparison.OrdinalIgnoreCase) + .Replace("_", " ", StringComparison.OrdinalIgnoreCase) + .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + + if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } } return false; diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs new file mode 100644 index 000000000..7c79a7c69 --- /dev/null +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider crypto) + { + _cryptographyProvider = crypto; + } + + public string Name => "Default"; + + public bool IsEnabled => true; + + public Task<ProviderAuthenticationResult> Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) + { + if (resolvedUser == null) + { + throw new Exception("Invalid username or password"); + } + + var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + public Task<bool> HasPassword(User user) + { + var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); + return Task.FromResult(hasConfiguredPassword); + } + + private bool IsPasswordEmpty(User user, string passwordHash) + { + return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + } + + public Task ChangePassword(User user, string newPassword) + { + string newPasswordHash = null; + + if (newPassword != null) + { + newPasswordHash = GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException("newPasswordHash"); + } + + user.Password = newPasswordHash; + + return Task.CompletedTask; + } + + public string GetPasswordHash(User user) + { + return string.IsNullOrEmpty(user.Password) + ? GetEmptyHashedString(user) + : user.Password; + } + + public string GetEmptyHashedString(User user) + { + return GetHashedString(user, string.Empty); + } + + /// <summary> + /// Gets the hashed string. + /// </summary> + public string GetHashedString(User user, string str) + { + var salt = user.Salt; + if (salt != null) + { + // return BCrypt.HashPassword(str, salt); + } + + // legacy + return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } + } +} diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs new file mode 100644 index 000000000..186ec63da --- /dev/null +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Library; + +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; } + + private Func<Task> _closeFn; + + public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn) + { + MediaSource = mediaSource; + EnableStreamSharing = false; + _closeFn = closeFn; + ConsumerCount = 1; + UniqueId = Guid.NewGuid().ToString("N"); + } + + public Task Close() + { + return _closeFn(); + } + + public Task Open(CancellationToken openCancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 2934a5147..31af9370c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -44,6 +44,9 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Tasks; +using Emby.Server.Implementations.Playlists; +using MediaBrowser.Providers.MediaInfo; +using MediaBrowser.Controller; namespace Emby.Server.Implementations.Library { @@ -71,12 +74,6 @@ namespace Emby.Server.Implementations.Library private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// <summary> - /// Gets the list of BasePluginFolders added by plugins - /// </summary> - /// <value>The plugin folders.</value> - private IVirtualFolderCreator[] PluginFolderCreators { get; set; } - - /// <summary> /// Gets the list of currently registered entity resolvers /// </summary> /// <value>The entity resolvers enumerable.</value> @@ -140,6 +137,7 @@ namespace Emby.Server.Implementations.Library private readonly Func<IProviderManager> _providerManagerFactory; private readonly Func<IUserViewManager> _userviewManager; public bool IsScanRunning { get; private set; } + private IServerApplicationHost _appHost; /// <summary> /// The _library items cache @@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Library /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> - public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager) + public LibraryManager(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager) { _logger = logger; _taskManager = taskManager; @@ -178,6 +176,7 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; + _appHost = appHost; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -195,14 +194,12 @@ namespace Emby.Server.Implementations.Library /// <param name="itemComparers">The item comparers.</param> /// <param name="postscanTasks">The postscan tasks.</param> public void AddParts(IEnumerable<IResolverIgnoreRule> rules, - IEnumerable<IVirtualFolderCreator> pluginFolders, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, IEnumerable<ILibraryPostScanTask> postscanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); - PluginFolderCreators = pluginFolders.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray(); IntroProviders = introProviders.ToArray(); @@ -302,7 +299,7 @@ namespace Emby.Server.Implementations.Library } else { - if (!(item is Video)) + if (!(item is Video) && !(item is LiveTvChannel)) { return; } @@ -311,13 +308,47 @@ namespace Emby.Server.Implementations.Library LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } - public async Task DeleteItem(BaseItem item, DeleteOptions options) + public void DeleteItem(BaseItem item, DeleteOptions options) + { + DeleteItem(item, options, false); + } + + public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var parent = item.GetOwner() ?? item.GetParent(); + + DeleteItem(item, options, parent, notifyParentItem); + } + + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { if (item == null) { throw new ArgumentNullException("item"); } + if (item.SourceType == SourceType.Channel) + { + if (options.DeleteFromExternalProvider) + { + try + { + var task = BaseItem.ChannelManager.DeleteItem(item); + Task.WaitAll(task); + } + catch (ArgumentException) + { + // channel no longer installed + } + } + options.DeleteFileLocation = false; + } + if (item is LiveTvProgram) { _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", @@ -335,10 +366,6 @@ namespace Emby.Server.Implementations.Library item.Id); } - var parent = item.IsOwnedItem ? item.GetOwner() : item.GetParent(); - - var locationType = item.LocationType; - var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false).ToList() : new List<BaseItem>(); @@ -361,7 +388,7 @@ namespace Emby.Server.Implementations.Library } } - if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual) + if (options.DeleteFileLocation && item.IsFileProtocol) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future @@ -407,33 +434,10 @@ namespace Emby.Server.Implementations.Library isRequiredForDelete = false; } - - if (parent != null) - { - var parentFolder = parent as Folder; - if (parentFolder != null) - { - await parentFolder.ValidateChildren(new SimpleProgress<double>(), CancellationToken.None, new MetadataRefreshOptions(_fileSystem), false).ConfigureAwait(false); - } - else - { - await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false); - } - } - } - else if (parent != null) - { - var parentFolder = parent as Folder; - if (parentFolder != null) - { - parentFolder.RemoveChild(item); - } - else - { - await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false); - } } + item.SetParent(null); + ItemRepository.DeleteItem(item.Id, CancellationToken.None); foreach (var child in children) { @@ -497,7 +501,7 @@ namespace Emby.Server.Implementations.Library private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive) { - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } @@ -544,7 +548,7 @@ namespace Emby.Server.Implementations.Library var fullPath = fileInfo.FullName; - if (string.IsNullOrWhiteSpace(collectionType) && parent != null) + if (string.IsNullOrEmpty(collectionType) && parent != null) { collectionType = GetContentTypeOverride(fullPath, true); } @@ -572,7 +576,26 @@ namespace Emby.Server.Implementations.Library // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + FileSystemMetadata[] files; + var isVf = args.IsVf; + + try + { + files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || isVf); + } + catch (Exception ex) + { + if (parent != null && parent.IsPhysicalRoot) + { + _logger.ErrorException("Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", ex, isPhysicalRoot, isVf); + + files = new FileSystemMetadata[] { }; + } + else + { + throw; + } + } // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action @@ -717,42 +740,43 @@ namespace Emby.Server.Implementations.Library } // Add in the plug-in folders - foreach (var child in PluginFolderCreators) + var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists"); + + _fileSystem.CreateDirectory(path); + + Folder folder = new PlaylistsFolder { - var folder = child.GetFolder(); + Path = path + }; - if (folder != null) + if (folder.Id.Equals(Guid.Empty)) + { + if (string.IsNullOrEmpty(folder.Path)) { - if (folder.Id == Guid.Empty) - { - if (string.IsNullOrWhiteSpace(folder.Path)) - { - folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); - } - else - { - folder.Id = GetNewItemId(folder.Path, folder.GetType()); - } - } + folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); + } + else + { + folder.Id = GetNewItemId(folder.Path, folder.GetType()); + } + } - var dbItem = GetItemById(folder.Id) as BasePluginFolder; + var dbItem = GetItemById(folder.Id) as BasePluginFolder; - if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) - { - folder = dbItem; - } + if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) + { + folder = dbItem; + } - if (folder.ParentId != rootFolder.Id) - { - folder.ParentId = rootFolder.Id; - folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - } + if (folder.ParentId != rootFolder.Id) + { + folder.ParentId = rootFolder.Id; + folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + } - rootFolder.AddVirtualChild(folder); + rootFolder.AddVirtualChild(folder); - RegisterItem(folder); - } - } + RegisterItem(folder); return rootFolder; } @@ -798,16 +822,18 @@ namespace Emby.Server.Implementations.Library // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } + //_logger.Info("FindByPath {0}", path); + var query = new InternalItemsQuery { Path = path, IsFolder = isFolder, - OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new Tuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), Limit = 1, DtoOptions = new DtoOptions(true) }; @@ -957,7 +983,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - CreateItem(item, CancellationToken.None); + CreateItem(item, null); } return item; @@ -997,7 +1023,7 @@ namespace Emby.Server.Implementations.Library // Just run the scheduled task so that the user can see it _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>(); - return Task.FromResult(true); + return Task.CompletedTask; } /// <summary> @@ -1031,48 +1057,45 @@ namespace Emby.Server.Implementations.Library } } - private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) + private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) { - _logger.Info("Validating media library"); - - // Ensure these objects are lazy loaded. - // Without this there is a deadlock that will need to be investigated var rootChildren = RootFolder.Children.ToList(); rootChildren = GetUserRootFolder().Children.ToList(); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); - progress.Report(.5); - // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false); - progress.Report(1); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); - progress.Report(2); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType<Folder>().ToList()) { await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); } - progress.Report(3); + } + + private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) + { + _logger.Info("Validating media library"); + + await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false); var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(pct => progress.Report(3 + pct * .72)); + innerProgress.RegisterAction(pct => progress.Report(pct * .96)); // Now validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false); - progress.Report(75); + progress.Report(96); innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25)); + innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04))); // Run post-scan tasks await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); @@ -1102,8 +1125,13 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => { - double innerPercent = currentNumComplete * 100 + pct; + double innerPercent = pct; + innerPercent /= 100; + innerPercent += currentNumComplete; + innerPercent /= numTasks; + innerPercent *= 100; + progress.Report(innerPercent); }); @@ -1163,7 +1191,19 @@ namespace Emby.Server.Implementations.Library Locations = _fileSystem.GetFilePaths(dir, false) .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) - .Select(_fileSystem.ResolveShortcut) + .Select(i => + { + try + { + return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i)); + } + catch (Exception ex) + { + _logger.ErrorException("Error resolving shortcut file {0}", ex, i); + return null; + } + }) + .Where(i => i != null) .OrderBy(i => i) .ToArray(), @@ -1197,7 +1237,7 @@ namespace Emby.Server.Implementations.Library { return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } /// <summary> @@ -1208,7 +1248,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="System.ArgumentNullException">id</exception> public BaseItem GetItemById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -1234,9 +1274,9 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1258,9 +1298,9 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1391,7 +1431,7 @@ namespace Emby.Server.Implementations.Library return; } - var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList(); + var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList(); if (parents.All(i => { @@ -1406,13 +1446,13 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); - query.AncestorIds = new string[] { }; + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); + query.AncestorIds = Array.Empty<Guid>(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + query.TopParentIds = new[] { Guid.NewGuid() }; } } } @@ -1430,9 +1470,9 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1472,23 +1512,23 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + query.TopParentIds = new[] { Guid.NewGuid() }; } } else { // We need to be able to query from any arbitrary ancestor up the tree - query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray(); + query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray(); // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { - query.AncestorIds = new[] { Guid.NewGuid().ToString("N") }; + query.AncestorIds = new[] { Guid.NewGuid() }; } } @@ -1498,22 +1538,21 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - !query.ParentId.HasValue && + query.ParentId.Equals(Guid.Empty) && query.ChannelIds.Length == 0 && query.TopParentIds.Length == 0 && - string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) && - string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey) && + string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && + string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && query.ItemIds.Length == 0) { var userViews = _userviewManager().GetUserViews(new UserViewQuery { - UserId = user.Id.ToString("N"), + UserId = user.Id, IncludeHidden = true, IncludeExternalContent = allowExternalContent + }); - }, CancellationToken.None).Result; - - query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray(); + query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray(); } } @@ -1527,48 +1566,38 @@ namespace Emby.Server.Implementations.Library { return new[] { view.Id }; } - if (string.Equals(view.ViewType, CollectionType.Channels)) - { - var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery - { - UserId = user.Id.ToString("N") - - }, CancellationToken.None).Result; - - return channelResult.Items.Select(i => i.Id); - } // Translate view into folders - if (view.DisplayParentId != Guid.Empty) + if (!view.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } - return new Guid[] { }; + return Array.Empty<Guid>(); } - if (view.ParentId != Guid.Empty) + if (!view.ParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } - return new Guid[] { }; + return Array.Empty<Guid>(); } // Handle grouping - if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) + if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) { - return user.RootFolder + return GetUserRootFolder() .GetChildren(user, true) .OfType<CollectionFolder>() - .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return new Guid[] { }; + return Array.Empty<Guid>(); } var collectionFolder = item as CollectionFolder; @@ -1582,7 +1611,7 @@ namespace Emby.Server.Implementations.Library { return new[] { topParent.Id }; } - return new Guid[] { }; + return Array.Empty<Guid>(); } /// <summary> @@ -1737,7 +1766,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<Tuple<string, SortOrder>> orderByList) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList) { var isFirst = true; @@ -1802,9 +1831,9 @@ namespace Emby.Server.Implementations.Library /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public void CreateItem(BaseItem item, CancellationToken cancellationToken) + public void CreateItem(BaseItem item, BaseItem parent) { - CreateItems(new[] { item }, item.GetParent(), cancellationToken); + CreateItems(new[] { item }, parent, CancellationToken.None); } /// <summary> @@ -1828,6 +1857,12 @@ namespace Emby.Server.Implementations.Library { foreach (var item in list) { + // With the live tv guide this just creates too much noise + if (item.SourceType != SourceType.Library) + { + continue; + } + try { ItemAdded(this, new ItemChangeEventArgs @@ -1854,46 +1889,65 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Updates the item. /// </summary> - /// <param name="item">The item.</param> - /// <param name="updateReason">The update reason.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public void UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken) + public void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - var locationType = item.LocationType; - if (locationType != LocationType.Remote && locationType != LocationType.Virtual) + foreach (var item in items) { - _providerManagerFactory().SaveMetadata(item, updateReason); - } + if (item.IsFileProtocol) + { + _providerManagerFactory().SaveMetadata(item, updateReason); + } - item.DateLastSaved = DateTime.UtcNow; + item.DateLastSaved = DateTime.UtcNow; - var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; - _logger.Debug("Saving {0} to database.", logName); + RegisterItem(item); + } - ItemRepository.SaveItem(item, cancellationToken); + //var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; + //_logger.Debug("Saving {0} to database.", logName); - RegisterItem(item); + ItemRepository.SaveItems(items, cancellationToken); if (ItemUpdated != null) { - try + foreach (var item in items) { - ItemUpdated(this, new ItemChangeEventArgs + // With the live tv guide this just creates too much noise + if (item.SourceType != SourceType.Library) { - Item = item, - Parent = item.GetParent(), - UpdateReason = updateReason - }); - } - catch (Exception ex) - { - _logger.ErrorException("Error in ItemUpdated event handler", ex); + continue; + } + + try + { + ItemUpdated(this, new ItemChangeEventArgs + { + Item = item, + Parent = parent, + UpdateReason = updateReason + }); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ItemUpdated event handler", ex); + } } } } /// <summary> + /// Updates the item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="updateReason">The update reason.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + { + UpdateItems(new List<BaseItem> { item }, parent, updateReason, cancellationToken); + } + + /// <summary> /// Reports the item removed. /// </summary> /// <param name="item">The item.</param> @@ -1995,12 +2049,12 @@ namespace Emby.Server.Implementations.Library public string GetContentType(BaseItem item) { string configuredContentType = GetConfiguredContentType(item, false); - if (!string.IsNullOrWhiteSpace(configuredContentType)) + if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); - if (!string.IsNullOrWhiteSpace(configuredContentType)) + if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } @@ -2011,14 +2065,14 @@ namespace Emby.Server.Implementations.Library { var type = GetTopFolderContentType(item); - if (!string.IsNullOrWhiteSpace(type)) + if (!string.IsNullOrEmpty(type)) { return type; } return item.GetParents() .Select(GetConfiguredContentType) - .LastOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .LastOrDefault(i => !string.IsNullOrEmpty(i)); } public string GetConfiguredContentType(BaseItem item) @@ -2043,7 +2097,7 @@ namespace Emby.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); + var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); if (nameValuePair != null) { return nameValuePair.Value; @@ -2058,16 +2112,21 @@ namespace Emby.Server.Implementations.Library return null; } - while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null) + while (!item.ParentId.Equals(Guid.Empty)) { - item = item.GetParent(); + var parent = item.GetParent(); + if (parent == null || parent is AggregateFolder) + { + break; + } + item = parent; } return GetUserRootFolder().Children .OfType<ICollectionFolder>() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path)) .Select(i => i.CollectionType) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); @@ -2076,18 +2135,16 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView(User user, string name, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - return GetNamedView(user, name, null, viewType, sortName, cancellationToken); + return GetNamedView(user, name, Guid.Empty, viewType, sortName); } public UserView GetNamedView(string name, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views"); + var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views"); path = Path.Combine(path, _fileSystem.GetValidFilename(viewType)); @@ -2111,32 +2168,15 @@ namespace Emby.Server.Implementations.Library ForcedSortName = sortName }; - CreateItem(item, cancellationToken); + CreateItem(item, null); refresh = true; } - if (!refresh) - { - refresh = DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - } - - if (!refresh && item.DisplayParentId != Guid.Empty) - { - var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; - } - if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) - { - // Not sure why this is necessary but need to figure it out - // View images are not getting utilized without this - ForceSave = true - - }, RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.Normal); } return item; @@ -2144,12 +2184,12 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView(User user, string name, - string parentId, + Guid parentId, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty); + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); + var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2174,19 +2214,16 @@ namespace Emby.Server.Implementations.Library UserId = user.Id }; - if (!string.IsNullOrWhiteSpace(parentId)) - { - item.DisplayParentId = new Guid(parentId); - } + item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2207,8 +2244,7 @@ namespace Emby.Server.Implementations.Library public UserView GetShadowView(BaseItem parent, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { if (parent == null) { @@ -2244,14 +2280,14 @@ namespace Emby.Server.Implementations.Library item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2271,19 +2307,19 @@ namespace Emby.Server.Implementations.Library } public UserView GetNamedView(string name, - string parentId, + Guid parentId, string viewType, string sortName, - string uniqueId, - CancellationToken cancellationToken) + string uniqueId) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } - var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty); - if (!string.IsNullOrWhiteSpace(uniqueId)) + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); + var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + if (!string.IsNullOrEmpty(uniqueId)) { idValues += uniqueId; } @@ -2310,12 +2346,9 @@ namespace Emby.Server.Implementations.Library ForcedSortName = sortName }; - if (!string.IsNullOrWhiteSpace(parentId)) - { - item.DisplayParentId = new Guid(parentId); - } + item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } @@ -2323,12 +2356,12 @@ namespace Emby.Server.Implementations.Library if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; - item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken); + item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2346,6 +2379,13 @@ namespace Emby.Server.Implementations.Library return item; } + public void AddExternalSubtitleStreams(List<MediaStream> streams, + string videoPath, + string[] files) + { + new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); + } + public bool IsVideoFile(string path, LibraryOptions libraryOptions) { var resolver = new VideoResolver(GetNamingOptions()); @@ -2370,19 +2410,25 @@ namespace Emby.Server.Implementations.Library public int? GetSeasonNumberFromPath(string path) { - return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber; + return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber; } - public bool FillMissingEpisodeNumbersFromPath(Episode episode) + public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { + var series = episode.Series; + bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + if (!isAbsoluteNaming.Value) + { + // In other words, no filter applied + isAbsoluteNaming = null; + } + var resolver = new EpisodeResolver(GetNamingOptions()); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; - var locationType = episode.LocationType; - - var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? - resolver.Resolve(episode.Path, isFolder) : + var episodeInfo = episode.IsFileProtocol ? + resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : new Emby.Naming.TV.EpisodeInfo(); if (episodeInfo == null) @@ -2428,105 +2474,67 @@ namespace Emby.Server.Implementations.Library changed = true; } } - - if (!episode.ParentIndexNumber.HasValue) - { - var season = episode.Season; - - if (season != null) - { - episode.ParentIndexNumber = season.IndexNumber; - } - - if (episode.ParentIndexNumber.HasValue) - { - changed = true; - } - } } else { - if (!episode.IndexNumber.HasValue) + if (!episode.IndexNumber.HasValue || forceRefresh) { - episode.IndexNumber = episodeInfo.EpisodeNumber; - - if (episode.IndexNumber.HasValue) + if (episode.IndexNumber != episodeInfo.EpisodeNumber) { changed = true; } + episode.IndexNumber = episodeInfo.EpisodeNumber; } - if (!episode.IndexNumberEnd.HasValue) + if (!episode.IndexNumberEnd.HasValue || forceRefresh) { - episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; - - if (episode.IndexNumberEnd.HasValue) + if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber) { changed = true; } + episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; } - if (!episode.ParentIndexNumber.HasValue) + if (!episode.ParentIndexNumber.HasValue || forceRefresh) { - episode.ParentIndexNumber = episodeInfo.SeasonNumber; - - if (!episode.ParentIndexNumber.HasValue) - { - var season = episode.Season; - - if (season != null) - { - episode.ParentIndexNumber = season.IndexNumber; - } - } - - if (episode.ParentIndexNumber.HasValue) + if (episode.ParentIndexNumber != episodeInfo.SeasonNumber) { changed = true; } + episode.ParentIndexNumber = episodeInfo.SeasonNumber; } } - return changed; - } - - public NamingOptions GetNamingOptions() - { - return GetNamingOptions(true); - } - - public NamingOptions GetNamingOptions(bool allowOptimisticEpisodeDetection) - { - if (!allowOptimisticEpisodeDetection) + if (!episode.ParentIndexNumber.HasValue) { - if (_namingOptionsWithoutOptimisticEpisodeDetection == null) - { - var namingOptions = new ExtendedNamingOptions(); + var season = episode.Season; - InitNamingOptions(namingOptions); - namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions - .Where(i => i.IsNamed && !i.IsOptimistic) - .ToList(); - - _namingOptionsWithoutOptimisticEpisodeDetection = namingOptions; + if (season != null) + { + episode.ParentIndexNumber = season.IndexNumber; } - return _namingOptionsWithoutOptimisticEpisodeDetection; + if (episode.ParentIndexNumber.HasValue) + { + changed = true; + } } + return changed; + } + + public NamingOptions GetNamingOptions() + { return GetNamingOptionsInternal(); } - private NamingOptions _namingOptionsWithoutOptimisticEpisodeDetection; private NamingOptions _namingOptions; private string[] _videoFileExtensions; private NamingOptions GetNamingOptionsInternal() { if (_namingOptions == null) { - var options = new ExtendedNamingOptions(); - - InitNamingOptions(options); + var options = new NamingOptions(); _namingOptions = options; _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray(); @@ -2535,27 +2543,6 @@ namespace Emby.Server.Implementations.Library return _namingOptions; } - private void InitNamingOptions(NamingOptions options) - { - // These cause apps to have problems - options.AudioFileExtensions.Remove(".m3u"); - options.AudioFileExtensions.Remove(".wpl"); - - //if (!libraryOptions.EnableArchiveMediaFiles) - { - options.AudioFileExtensions.Remove(".rar"); - options.AudioFileExtensions.Remove(".zip"); - } - - //if (!libraryOptions.EnableArchiveMediaFiles) - { - options.VideoFileExtensions.Remove(".rar"); - options.VideoFileExtensions.Remove(".zip"); - } - - options.VideoFileExtensions.Add(".tp"); - } - public ItemLookupInfo ParseName(string name) { var resolver = new VideoResolver(GetNamingOptions()); @@ -2606,12 +2593,11 @@ namespace Emby.Server.Implementations.Library { video = dbItem; } - else - { - // item is new - video.ExtraType = ExtraType.Trailer; - } - video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer }; + + video.ParentId = Guid.Empty; + video.OwnerId = owner.Id; + video.ExtraType = ExtraType.Trailer; + video.TrailerTypes = new [] { TrailerType.LocalTrailer }; return video; @@ -2625,7 +2611,7 @@ namespace Emby.Server.Implementations.Library { var namingOptions = GetNamingOptions(); - var files = fileSystemChildren.Where(i => i.IsDirectory) + var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => ExtrasSubfolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); @@ -2653,6 +2639,9 @@ namespace Emby.Server.Implementations.Library video = dbItem; } + video.ParentId = Guid.Empty; + video.OwnerId = owner.Id; + SetExtraTypeFromFilename(video); return video; @@ -2756,7 +2745,7 @@ namespace Emby.Server.Implementations.Library private void SetExtraTypeFromFilename(Video item) { - var resolver = new ExtraResolver(GetNamingOptions(), new RegexProvider()); + var resolver = new ExtraResolver(GetNamingOptions()); var result = resolver.GetExtraInfo(item.Path); @@ -2841,7 +2830,7 @@ namespace Emby.Server.Implementations.Library ItemRepository.UpdatePeople(item.Id, people); } - public async Task<ItemImageInfo> ConvertImageToLocal(IHasMetadata item, ItemImageInfo image, int imageIndex) + public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) { foreach (var url in image.Path.Split('|')) { @@ -2872,7 +2861,7 @@ namespace Emby.Server.Implementations.Library throw new InvalidOperationException(); } - public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) + public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -2910,7 +2899,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType + ".collection"); - _fileSystem.WriteAllBytes(path, new byte[] { }); + _fileSystem.WriteAllBytes(path, Array.Empty<byte>()); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); @@ -2925,26 +2914,30 @@ namespace Emby.Server.Implementations.Library } finally { - Task.Run(() => + if (refreshLibrary) { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); + await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); - _libraryMonitorFactory().Start(); - } - }); + StartScanInBackground(); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitorFactory().Start(); + } } } + private void StartScanInBackground() + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); + }); + } + private bool ValidateNetworkPath(string path) { //if (Environment.OSVersion.Platform == PlatformID.Win32NT) @@ -3003,7 +2996,7 @@ namespace Emby.Server.Implementations.Library lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); } - _fileSystem.CreateShortcut(lnk, path); + _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); RemoveContentTypeOverrides(path); @@ -3079,7 +3072,7 @@ namespace Emby.Server.Implementations.Library } } - public void RemoveVirtualFolder(string name, bool refreshLibrary) + public async Task RemoveVirtualFolder(string name, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -3103,23 +3096,20 @@ namespace Emby.Server.Implementations.Library } finally { - Task.Run(() => + CollectionFolder.OnCollectionFolderChange(); + + if (refreshLibrary) { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); + await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); - _libraryMonitorFactory().Start(); - } - }); + StartScanInBackground(); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitorFactory().Start(); + } } } @@ -3157,7 +3147,7 @@ namespace Emby.Server.Implementations.Library public void RemoveMediaPath(string virtualFolderName, string mediaPath) { - if (string.IsNullOrWhiteSpace(mediaPath)) + if (string.IsNullOrEmpty(mediaPath)) { throw new ArgumentNullException("mediaPath"); } @@ -3172,7 +3162,7 @@ namespace Emby.Server.Implementations.Library var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) { diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs new file mode 100644 index 000000000..e027e133f --- /dev/null +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -0,0 +1,181 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Configuration; +using System.IO; +using MediaBrowser.Common.Extensions; + +namespace Emby.Server.Implementations.Library +{ + public class LiveStreamHelper + { + private readonly IMediaEncoder _mediaEncoder; + private readonly ILogger _logger; + + private IJsonSerializer _json; + private IApplicationPaths _appPaths; + + public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths) + { + _mediaEncoder = mediaEncoder; + _logger = logger; + _json = json; + _appPaths = appPaths; + } + + public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var now = DateTime.UtcNow; + + MediaInfo mediaInfo = null; + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + + if (!string.IsNullOrEmpty(cacheKey)) + { + try + { + mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath); + + //_logger.Debug("Found cached media info"); + } + catch + { + } + } + + if (mediaInfo == null) + { + if (addProbeDelay) + { + var delayMs = mediaSource.AnalyzeDurationMs ?? 0; + delayMs = Math.Max(3000, delayMs); + if (delayMs > 0) + { + _logger.Info("Waiting {0}ms before probing the live stream", delayMs); + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + } + } + + mediaSource.AnalyzeDurationMs = 3000; + + mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + + }, cancellationToken).ConfigureAwait(false); + + if (cacheFilePath != null) + { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); + _json.SerializeToFile(mediaInfo, cacheFilePath); + + //_logger.Debug("Saved media info to {0}", cacheFilePath); + } + } + + var mediaStreams = mediaInfo.MediaStreams; + + if (!string.IsNullOrEmpty(cacheKey)) + { + var newList = new List<MediaStream>(); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1)); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1)); + + foreach (var stream in newList) + { + stream.Index = -1; + stream.Language = null; + } + + mediaStreams = newList; + } + + _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + mediaSource.Bitrate = mediaInfo.Bitrate; + mediaSource.Container = mediaInfo.Container; + mediaSource.Formats = mediaInfo.Formats; + mediaSource.MediaStreams = mediaStreams; + mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks; + mediaSource.Size = mediaInfo.Size; + mediaSource.Timestamp = mediaInfo.Timestamp; + mediaSource.Video3DFormat = mediaInfo.Video3DFormat; + mediaSource.VideoType = mediaInfo.VideoType; + + mediaSource.DefaultSubtitleStreamIndex = null; + + // Null this out so that it will be treated like a live stream + if (!originalRuntime.HasValue) + { + mediaSource.RunTimeTicks = null; + } + + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) + { + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + + // This is coming up false and preventing stream copy + videoStream.IsAVC = null; + } + + mediaSource.AnalyzeDurationMs = 3000; + + // Try to estimate this + mediaSource.InferTotalBitrate(true); + } + + public Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, bool addProbeDelay, CancellationToken cancellationToken) + { + return AddMediaInfoWithProbe(mediaSource, isAudio, null, addProbeDelay, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs deleted file mode 100644 index 4830da8fc..000000000 --- a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs +++ /dev/null @@ -1,105 +0,0 @@ -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; - -namespace Emby.Server.Implementations.Library -{ - public class LocalTrailerPostScanTask : ILibraryPostScanTask - { - private readonly ILibraryManager _libraryManager; - private readonly IChannelManager _channelManager; - - public LocalTrailerPostScanTask(ILibraryManager libraryManager, IChannelManager channelManager) - { - _libraryManager = libraryManager; - _channelManager = channelManager; - } - - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(BoxSet).Name, typeof(Game).Name, typeof(Movie).Name, typeof(Series).Name }, - Recursive = true, - DtoOptions = new DtoOptions(true) - - }).OfType<IHasTrailers>().ToList(); - - var trailerTypes = Enum.GetNames(typeof(TrailerType)) - .Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)) - .Except(new[] { TrailerType.LocalTrailer }) - .ToArray(); - - var trailers = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Trailer).Name }, - TrailerTypes = trailerTypes, - Recursive = true, - DtoOptions = new DtoOptions(false) - - }); - - var numComplete = 0; - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - AssignTrailers(item, trailers); - - numComplete++; - double percent = numComplete; - percent /= items.Count; - progress.Report(percent * 100); - } - - progress.Report(100); - } - - private void AssignTrailers(IHasTrailers item, IEnumerable<BaseItem> channelTrailers) - { - if (item is Game) - { - return; - } - - var imdbId = item.GetProviderId(MetadataProviders.Imdb); - var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - - var trailers = channelTrailers.Where(i => - { - if (!string.IsNullOrWhiteSpace(imdbId) && - string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - if (!string.IsNullOrWhiteSpace(tmdbId) && - string.Equals(tmdbId, i.GetProviderId(MetadataProviders.Tmdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - return false; - }); - - var trailerIds = trailers.Select(i => i.Id) - .ToArray(); - - if (!trailerIds.SequenceEqual(item.RemoteTrailerIds)) - { - item.RemoteTrailerIds = trailerIds; - - var baseItem = (BaseItem)item; - baseItem.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - } - } - } -} diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 688da5764..0dc436800 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; @@ -16,6 +17,11 @@ using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Threading; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using System.IO; +using System.Globalization; +using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.Library { @@ -31,8 +37,11 @@ namespace Emby.Server.Implementations.Library private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; private readonly ITimerFactory _timerFactory; + private readonly Func<IMediaEncoder> _mediaEncoder; + private ILocalizationManager _localizationManager; + private IApplicationPaths _appPaths; - public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory) + public MediaSourceManager(IItemRepository itemRepo, IApplicationPaths applicationPaths, ILocalizationManager localizationManager, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory, Func<IMediaEncoder> mediaEncoder) { _itemRepo = itemRepo; _userManager = userManager; @@ -42,6 +51,9 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _userDataManager = userDataManager; _timerFactory = timerFactory; + _mediaEncoder = mediaEncoder; + _localizationManager = localizationManager; + _appPaths = applicationPaths; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -109,20 +121,23 @@ namespace Emby.Server.Implementations.Library return streams; } - public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken) + public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { - var item = _libraryManager.GetItemById(id); + var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); - var hasMediaSources = (IHasMediaSources)item; - User user = null; - - if (!string.IsNullOrWhiteSpace(userId)) + if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { - user = _userManager.GetUserById(userId); + await item.RefreshMetadata(new MediaBrowser.Controller.Providers.MetadataRefreshOptions(_fileSystem) + { + EnableRemoteContentProbe = true, + MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh + + }, cancellationToken).ConfigureAwait(false); + + mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); } - var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user); - var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false); + var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false); var list = new List<MediaSourceInfo>(); @@ -132,24 +147,13 @@ namespace Emby.Server.Implementations.Library { if (user != null) { - SetUserProperties(hasMediaSources, source, user); - } - if (source.Protocol == MediaProtocol.File) - { - // TODO: Path substitution - if (!_fileSystem.FileExists(source.Path)) - { - source.SupportsDirectStream = false; - } - } - else if (source.Protocol == MediaProtocol.Http) - { - // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash - source.SupportsDirectStream = false; + SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); } - else + + // Validate that this is actually possible + if (source.SupportsDirectStream) { - source.SupportsDirectStream = false; + source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol); } list.Add(source); @@ -169,10 +173,63 @@ namespace Emby.Server.Implementations.Library } } - return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder); + return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList(); + } + + public MediaProtocol GetPathProtocol(string path) + { + if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) + { + 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; + } + + return _fileSystem.IsPathFile(path) ? MediaProtocol.File : MediaProtocol.Http; } - private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + public bool SupportsDirectStream(string path, MediaProtocol protocol) + { + if (protocol == MediaProtocol.File) + { + return true; + } + + if (protocol == MediaProtocol.Http) + { + if (path != null) + { + if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) + { + return false; + } + + return true; + } + } + + return false; + } + + private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken) { var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -180,7 +237,7 @@ namespace Emby.Server.Implementations.Library return results.SelectMany(i => i.ToList()); } - private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken) + private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken) { try { @@ -207,78 +264,65 @@ namespace Emby.Server.Implementations.Library { var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter; - if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.OpenToken = prefix + mediaSource.OpenToken; } - if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId; } } - public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) + public async Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) { - if (!string.IsNullOrWhiteSpace(liveStreamId)) + if (!string.IsNullOrEmpty(liveStreamId)) { return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false); } - //await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - //try - //{ - // var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); - // if (stream != null) - // { - // return stream.MediaSource; - // } - //} - //finally - //{ - // _liveStreamSemaphore.Release(); - //} - - var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video }, - CancellationToken.None).ConfigureAwait(false); + var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false); return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public List<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) + public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) { if (item == null) { throw new ArgumentNullException("item"); } - if (!(item is Video)) - { - return item.GetMediaSources(enablePathSubstitution); - } + var hasMediaSources = (IHasMediaSources)item; - var sources = item.GetMediaSources(enablePathSubstitution); + var sources = hasMediaSources.GetMediaSources(enablePathSubstitution); if (user != null) { foreach (var source in sources) { - SetUserProperties(item, source, user); + SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); } } return sources; } - private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user) + private string[] NormalizeLanguage(string language) { - var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + if (language != null) + { + var culture = _localizationManager.FindLanguageInfo(language); + if (culture != null) + { + return culture.ThreeLetterISOLanguageNames; + } - var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; + return new string[] { language }; + } - SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); - SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); + return Array.Empty<string>(); } private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) @@ -293,9 +337,9 @@ namespace Emby.Server.Implementations.Library return; } } - + var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) - ? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference }; + ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null @@ -325,12 +369,37 @@ namespace Emby.Server.Implementations.Library } var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) - ? new string[] { } - : new[] { user.Configuration.AudioLanguagePreference }; + ? Array.Empty<string>() + : NormalizeLanguage(user.Configuration.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); } + public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + { + // Item would only be null if the app didn't supply ItemId as part of the live stream open request + var mediaType = item == null ? MediaType.Video : item.MediaType; + + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + + var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; + + SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); + SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); + } + else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + if (audio != null) + { + source.DefaultAudioStreamIndex = audio.Index; + } + } + } + private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) { return sources.OrderBy(i => @@ -352,55 +421,157 @@ namespace Emby.Server.Implementations.Library .ToList(); } - private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + MediaSourceInfo mediaSource; + ILiveStream liveStream; + try { var tuple = GetProvider(request.OpenToken); var provider = tuple.Item1; - var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, request.EnableMediaProbe, cancellationToken).ConfigureAwait(false); + var currentLiveStreams = _openStreams.Values.ToList(); + + liveStream = await provider.OpenMediaSource(tuple.Item2, currentLiveStreams, cancellationToken).ConfigureAwait(false); - var mediaSource = mediaSourceTuple.Item1; + mediaSource = liveStream.MediaSource; - if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) + // Validate that this is actually possible + if (mediaSource.SupportsDirectStream) { - throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name)); + mediaSource.SupportsDirectStream = SupportsDirectStream(mediaSource.Path, mediaSource.Protocol); } SetKeyProperties(provider, mediaSource); - var info = new LiveStreamInfo + _openStreams[mediaSource.LiveStreamId] = liveStream; + } + finally + { + _liveStreamSemaphore.Release(); + } + + // TODO: Don't hardcode this + var isAudio = false; + + try + { + if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) { - Id = mediaSource.LiveStreamId, - MediaSource = mediaSource, - DirectStreamProvider = mediaSourceTuple.Item2 - }; - - _openStreams[mediaSource.LiveStreamId] = info; - - var json = _jsonSerializer.SerializeToString(mediaSource); - _logger.Debug("Live stream opened: " + json); - var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - - if (!string.IsNullOrWhiteSpace(request.UserId)) - { - var user = _userManager.GetUserById(request.UserId); - var item = string.IsNullOrWhiteSpace(request.ItemId) - ? null - : _libraryManager.GetItemById(request.ItemId); - SetUserProperties(item, clone, user); + AddMediaInfo(mediaSource, isAudio); + } + else + { + // hack - these two values were taken from LiveTVMediaSourceProvider + var cacheKey = request.OpenToken; + + await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths).AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken).ConfigureAwait(false); } + } + catch (Exception ex) + { + _logger.ErrorException("Error probing live tv stream", ex); + AddMediaInfo(mediaSource, isAudio); + } + + var json = _jsonSerializer.SerializeToString(mediaSource); + _logger.Info("Live stream opened: " + json); + var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = _userManager.GetUserById(request.UserId); + var item = request.ItemId.Equals(Guid.Empty) + ? null + : _libraryManager.GetItemById(request.ItemId); + SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); + } + + return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse + { + MediaSource = clone + + }, liveStream as IDirectStreamProvider); + } + + private void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) + { + mediaSource.DefaultSubtitleStreamIndex = null; + + // Null this out so that it will be treated like a live stream + if (mediaSource.IsInfiniteStream) + { + mediaSource.RunTimeTicks = null; + } - return new LiveStreamResponse + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) { - MediaSource = clone - }; + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + } + + // Try to estimate this + mediaSource.InferTotalBitrate(); + } + + public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) + { + await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var info = _openStreams.Values.FirstOrDefault(i => + { + var liveStream = i as ILiveStream; + if (liveStream != null) + { + return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); + } + + return false; + }); + + return info as IDirectStreamProvider; } finally { @@ -408,23 +579,207 @@ namespace Emby.Server.Implementations.Library } } + public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + { + var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false); + return result.Item1; + } + + public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken) + { + var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); + + var mediaSource = liveStreamInfo.MediaSource; + + if (liveStreamInfo is IDirectStreamProvider) + { + var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + ExtractChapters = false, + MediaType = DlnaProfileType.Video + + }, cancellationToken).ConfigureAwait(false); + + mediaSource.MediaStreams = info.MediaStreams; + mediaSource.Container = info.Container; + mediaSource.Bitrate = info.Bitrate; + } + + return mediaSource; + } + + public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var now = DateTime.UtcNow; + + MediaInfo mediaInfo = null; + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + + if (!string.IsNullOrEmpty(cacheKey)) + { + try + { + mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath); + + //_logger.Debug("Found cached media info"); + } + catch (Exception ex) + { + } + } + + if (mediaInfo == null) + { + if (addProbeDelay) + { + var delayMs = mediaSource.AnalyzeDurationMs ?? 0; + delayMs = Math.Max(3000, delayMs); + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + } + + if (isLiveStream) + { + mediaSource.AnalyzeDurationMs = 3000; + } + + mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + + }, cancellationToken).ConfigureAwait(false); + + if (cacheFilePath != null) + { + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFilePath)); + _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath); + + //_logger.Debug("Saved media info to {0}", cacheFilePath); + } + } + + var mediaStreams = mediaInfo.MediaStreams; + + if (isLiveStream && !string.IsNullOrEmpty(cacheKey)) + { + var newList = new List<MediaStream>(); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1)); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1)); + + foreach (var stream in newList) + { + stream.Index = -1; + stream.Language = null; + } + + mediaStreams = newList; + } + + _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + mediaSource.Bitrate = mediaInfo.Bitrate; + mediaSource.Container = mediaInfo.Container; + mediaSource.Formats = mediaInfo.Formats; + mediaSource.MediaStreams = mediaStreams; + mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks; + mediaSource.Size = mediaInfo.Size; + mediaSource.Timestamp = mediaInfo.Timestamp; + mediaSource.Video3DFormat = mediaInfo.Video3DFormat; + mediaSource.VideoType = mediaInfo.VideoType; + + mediaSource.DefaultSubtitleStreamIndex = null; + + if (isLiveStream) + { + // Null this out so that it will be treated like a live stream + if (!originalRuntime.HasValue) + { + mediaSource.RunTimeTicks = null; + } + } + + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) + { + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + + // This is coming up false and preventing stream copy + videoStream.IsAVC = null; + } + + if (isLiveStream) + { + mediaSource.AnalyzeDurationMs = 3000; + } + + // Try to estimate this + mediaSource.InferTotalBitrate(true); + } + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } - _logger.Debug("Getting already opened live stream {0}", id); + var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); + } + + private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - LiveStreamInfo info; + ILiveStream info; if (_openStreams.TryGetValue(id, out info)) { - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider); + return info; } else { @@ -443,26 +798,9 @@ namespace Emby.Server.Implementations.Library return result.Item1; } - private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId) - { - _logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name); - - try - { - await provider.CloseMediaSource(streamId).ConfigureAwait(false); - } - catch (NotImplementedException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error closing live stream {0}", ex, streamId); - } - } - public async Task CloseLiveStream(string id) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } @@ -471,18 +809,22 @@ namespace Emby.Server.Implementations.Library try { - LiveStreamInfo current; + ILiveStream liveStream; - if (_openStreams.TryGetValue(id, out current)) + if (_openStreams.TryGetValue(id, out liveStream)) { - _openStreams.Remove(id); - current.Closed = true; + liveStream.ConsumerCount--; - if (current.MediaSource.RequiresClosing) + _logger.Info("Live stream {0} consumer count is now {1}", liveStream.OriginalStreamId, liveStream.ConsumerCount); + + if (liveStream.ConsumerCount <= 0) { - var tuple = GetProvider(id); + _openStreams.Remove(id); + + _logger.Info("Closing live stream {0}", id); - await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false); + await liveStream.Close().ConfigureAwait(false); + _logger.Info("Live stream {0} closed successfully", id); } } } @@ -497,7 +839,7 @@ namespace Emby.Server.Implementations.Library private Tuple<IMediaSourceProvider, string> GetProvider(string key) { - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrEmpty(key)) { throw new ArgumentException("key"); } @@ -518,7 +860,6 @@ namespace Emby.Server.Implementations.Library public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } private readonly object _disposeLock = new object(); @@ -541,13 +882,5 @@ namespace Emby.Server.Implementations.Library } } } - - private class LiveStreamInfo - { - public string Id; - public bool Closed; - public MediaSourceInfo MediaSource; - public IDirectStreamProvider DirectStreamProvider; - } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs new file mode 100644 index 000000000..5d4c5a452 --- /dev/null +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -0,0 +1,217 @@ +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Server.Implementations.Library +{ + public static class MediaStreamSelector + { + public static int? GetDefaultAudioStreamIndex(List<MediaStream> streams, string[] preferredLanguages, bool preferDefaultTrack) + { + streams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages) + .ToList(); + + if (preferDefaultTrack) + { + var defaultStream = streams.FirstOrDefault(i => i.IsDefault); + + if (defaultStream != null) + { + return defaultStream.Index; + } + } + + var stream = streams.FirstOrDefault(); + + if (stream != null) + { + return stream.Index; + } + + return null; + } + + public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams, + string[] preferredLanguages, + SubtitlePlaybackMode mode, + string audioTrackLanguage) + { + streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) + .ToList(); + + MediaStream stream = null; + + if (mode == SubtitlePlaybackMode.None) + { + return null; + } + + if (mode == SubtitlePlaybackMode.Default) + { + // Prefer embedded metadata over smart logic + + stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => s.IsForced) ?? + streams.FirstOrDefault(s => s.IsDefault); + + // if the audio language is not understood by the user, load their preferred subs, if there are any + if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + } + } + else if (mode == SubtitlePlaybackMode.Smart) + { + // Prefer smart logic over embedded metadata + + // if the audio language is not understood by the user, load their preferred subs, if there are any + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + } + } + else if (mode == SubtitlePlaybackMode.Always) + { + // always load the most suitable full subtitles + stream = streams.FirstOrDefault(s => !s.IsForced); + } + else if (mode == SubtitlePlaybackMode.OnlyForced) + { + // always load the most suitable full subtitles + stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => s.IsForced); + } + + // load forced subs if we have found no suitable full subtitles + stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); + + if (stream != null) + { + return stream.Index; + } + + return null; + } + + private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences) + { + // Give some preferance to external text subs for better performance + return streams.Where(i => i.Type == type) + .OrderBy(i => + { + var index = FindIndex(languagePreferences, i.Language); + + return index == -1 ? 100 : index; + }) + .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) + .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) + .ThenBy(i => i.Index); + } + + public static void SetSubtitleStreamScores(List<MediaStream> streams, + string[] preferredLanguages, + SubtitlePlaybackMode mode, + string audioTrackLanguage) + { + if (mode == SubtitlePlaybackMode.None) + { + return; + } + + streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) + .ToList(); + + var filteredStreams = new List<MediaStream>(); + + if (mode == SubtitlePlaybackMode.Default) + { + // Prefer embedded metadata over smart logic + filteredStreams = streams.Where(s => s.IsForced || s.IsDefault) + .ToList(); + } + else if (mode == SubtitlePlaybackMode.Smart) + { + // Prefer smart logic over embedded metadata + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } + } + else if (mode == SubtitlePlaybackMode.Always) + { + // always load the most suitable full subtitles + filteredStreams = streams.Where(s => !s.IsForced) + .ToList(); + } + else if (mode == SubtitlePlaybackMode.OnlyForced) + { + // always load the most suitable full subtitles + filteredStreams = streams.Where(s => s.IsForced).ToList(); + } + + // load forced subs if we have found no suitable full subtitles + if (filteredStreams.Count == 0) + { + filteredStreams = streams + .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + foreach (var stream in filteredStreams) + { + stream.Score = GetSubtitleScore(stream, preferredLanguages); + } + } + + private static int FindIndex(string[] list, string value) + { + for (var i=0; i< list.Length; i++) + { + if (string.Equals(list[i], value, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + private static int GetSubtitleScore(MediaStream stream, string[] languagePreferences) + { + var values = new List<int>(); + + var index = FindIndex(languagePreferences, stream.Language); + + values.Add(index == -1 ? 0 : 100 - index); + + values.Add(stream.IsForced ? 1 : 0); + values.Add(stream.IsDefault ? 1 : 0); + values.Add(stream.SupportsExternalStream ? 1 : 0); + values.Add(stream.IsTextSubtitleStream ? 1 : 0); + values.Add(stream.IsExternal ? 1 : 0); + + values.Reverse(); + var scale = 1; + var score = 0; + + foreach (var value in values) + { + score += scale * (value + 1); + scale *= 10; + } + + return score; + } + + private static int GetBooleanOrderBy(bool value) + { + return value ? 0 : 1; + } + } +} diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 1cbf4235a..1319ee6f4 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -67,19 +67,19 @@ namespace Emby.Server.Implementations.Library { try { - return _libraryManager.GetMusicGenre(i).Id.ToString("N"); + return _libraryManager.GetMusicGenre(i).Id; } catch { - return null; + return Guid.Empty; } - }).Where(i => i != null); + }).Where(i => !i.Equals(Guid.Empty)).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromGenreIds(IEnumerable<string> genreIds, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library Limit = 200, - OrderBy = new [] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, + OrderBy = new [] { new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, DtoOptions = dtoOptions @@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library var genre = item as MusicGenre; if (genre != null) { - return GetInstantMixFromGenreIds(new[] { item.Id.ToString("N") }, user, dtoOptions); + return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } var playlist = item as Playlist; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index d0096de0c..14b28966a 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set - if (string.IsNullOrWhiteSpace(item.Path)) + if (string.IsNullOrEmpty(item.Path)) { throw new ArgumentException("Item must have a Path"); } @@ -108,17 +108,6 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// The MB name regex - /// </summary> - private static readonly Regex MbNameRegex = new Regex(@"(\[.*?\])"); - - internal static string StripBrackets(string inputString) - { - var output = MbNameRegex.Replace(inputString, string.Empty).Trim(); - return Regex.Replace(output, @"\s+", " "); - } - - /// <summary> /// Ensures DateCreated and DateModified have values /// </summary> /// <param name="fileSystem">The file system.</param> @@ -140,7 +129,7 @@ namespace Emby.Server.Implementations.Library } // See if a different path came out of the resolver than what went in - if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase)) + if (!fileSystem.AreEqual(args.Path, item.Path)) { var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null; @@ -173,7 +162,14 @@ namespace Emby.Server.Implementations.Library // directoryService.getFile may return null if (info != null) { - item.DateCreated = fileSystem.GetCreationTimeUtc(info); + var dateCreated = fileSystem.GetCreationTimeUtc(info); + + if (dateCreated.Equals(DateTime.MinValue)) + { + dateCreated = DateTime.UtcNow; + } + + item.DateCreated = dateCreated; } } else diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index d30aaa133..8872bd641 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -101,13 +101,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (LibraryManager.IsAudioFile(args.Path, libraryOptions)) { - if (string.Equals(Path.GetExtension(args.Path), ".cue", StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(args.Path); + + if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; } - var isMixedCollectionType = string.IsNullOrWhiteSpace(collectionType); + var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions)) @@ -134,6 +136,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item != null) { + item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index b8ec41805..a33f101ae 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -52,14 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <returns>MusicAlbum.</returns> protected override MusicAlbum Resolve(ItemResolveArgs args) { - if (!args.IsDirectory) return null; - - // Avoid mis-identifying top folders - if (args.HasParent<MusicAlbum>()) return null; - if (args.Parent.IsRoot) return null; - var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); // If there's a collection type and it's not music, don't allow it. @@ -68,6 +61,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } + if (!args.IsDirectory) return null; + + // Avoid mis-identifying top folders + if (args.HasParent<MusicAlbum>()) return null; + if (args.Parent.IsRoot) return null; + return IsMusicAlbum(args) ? new MusicAlbum() : null; } @@ -117,24 +116,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { if (allowSubfolders) { + if (notMultiDisc) + { + continue; + } + var path = fileSystemInfo.FullName; - var isMultiDisc = IsMultiDiscFolder(path, libraryOptions); + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - if (isMultiDisc) + if (hasMusic) { - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - - if (hasMusic) + if (IsMultiDiscFolder(path, libraryOptions)) { logger.Debug("Found multi-disc folder: " + path); discSubfolderCount++; } - } - else - { - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - - if (hasMusic) + else { // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album notMultiDisc = true; diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 7e960f85e..556748183 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -184,11 +184,6 @@ namespace Emby.Server.Implementations.Library.Resolvers else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase)) { video.VideoType = VideoType.BluRay; - video.IsHD = true; - } - else if (string.Equals(videoInfo.StubType, "hdtv", StringComparison.OrdinalIgnoreCase)) - { - video.IsHD = true; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index df441c5ed..b9aca1417 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -4,6 +4,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using System; using System.IO; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Library.Resolvers.Movies { @@ -30,14 +31,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { return null; } - - if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || - args.ContainsFileSystemEntryByName("collection.xml")) + + if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml")) { return new BoxSet { Path = args.Path, - Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path)) + Name = Path.GetFileName(args.Path).Replace("[boxset]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() }; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index d74235ec7..1394e3858 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false); } - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { // Owned items should just use the plain video type if (parent == null) @@ -113,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies foreach (var child in fileSystemEntries) { // This is a hack but currently no better way to resolve a sometimes ambiguous situation - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase)) @@ -126,6 +126,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { leftOver.Add(child); } + else if (IsIgnored(child.Name)) + { + + } else { files.Add(child); @@ -172,6 +176,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } + private bool IsIgnored(string filename) + { + // Ignore samples + var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) + .Replace("-", " ", StringComparison.OrdinalIgnoreCase) + .Replace("_", " ", StringComparison.OrdinalIgnoreCase) + .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + + if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } + + return false; + } + private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file) { return result.Any(i => ContainsFile(i, file)); @@ -317,7 +337,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies //we need to only look at the name of this actual item (not parents) var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath); - if (!string.IsNullOrWhiteSpace(justName)) + if (!string.IsNullOrEmpty(justName)) { // check for tmdb id var tmdbid = justName.GetAttributeValue("tmdbid"); @@ -328,7 +348,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (!string.IsNullOrWhiteSpace(item.Path)) + if (!string.IsNullOrEmpty(item.Path)) { // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name) var imdbid = item.Path.GetAttributeValue("imdbid"); @@ -395,16 +415,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Set3DFormat(movie); return movie; } - else if (supportPhotos && !child.IsHidden && PhotoResolver.IsImageFile(child.FullName, _imageProcessor)) + else if (supportPhotos && PhotoResolver.IsImageFile(child.FullName, _imageProcessor)) { photos.Add(child); } } // TODO: Allow GetMultiDiscMovie in here - var supportsMultiVersion = !string.Equals(collectionType, CollectionType.HomeVideos) && - !string.Equals(collectionType, CollectionType.Photos) && - !string.Equals(collectionType, CollectionType.MusicVideos); + var supportsMultiVersion = true; var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); @@ -532,7 +550,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { return false; } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 48f5802a9..e3cce5f4b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -94,7 +94,8 @@ namespace Emby.Server.Implementations.Library.Resolvers "backdrop", "poster", "cover", - "logo" + "logo", + "default" }; internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 8c59cf20f..e66c9f087 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -2,11 +2,20 @@ using MediaBrowser.Controller.Playlists; using System; using System.IO; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Entities; +using System.Linq; namespace Emby.Server.Implementations.Library.Resolvers { public class PlaylistResolver : FolderResolver<Playlist> { + private string[] SupportedCollectionTypes = new string[] { + + string.Empty, + CollectionType.Music + }; + /// <summary> /// Resolves the specified args. /// </summary> @@ -31,10 +40,26 @@ namespace Emby.Server.Implementations.Library.Resolvers return new Playlist { Path = args.Path, - Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path)) + Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() }; } } + else + { + if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var extension = Path.GetExtension(args.Path); + if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return new Playlist + { + Path = args.Path, + Name = Path.GetFileNameWithoutExtension(args.Path), + IsInMixedFolder = true + }; + } + } + } return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 3bad69b56..d8343f7c6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -50,24 +50,29 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; + var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true); + var season = new Season { - IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, true, true).SeasonNumber, + IndexNumber = seasonParserResult.SeasonNumber, SeriesId = series.Id, SeriesName = series.Name }; - if (season.IndexNumber.HasValue) + if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder) { var resolver = new Emby.Naming.TV.EpisodeResolver(namingOptions); - var episodeInfo = resolver.Resolve(path, true); + var folderName = System.IO.Path.GetFileName(path); + var testPath = "\\\\test\\" + folderName; + + var episodeInfo = resolver.Resolve(testPath, true); if (episodeInfo != null) { if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) { - _logger.Info("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", + _logger.Debug("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", path, episodeInfo.SeasonNumber.Value, episodeInfo.EpisodeNumber.Value); @@ -75,7 +80,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } } + } + if (season.IndexNumber.HasValue) + { var seasonNumber = season.IndexNumber.Value; season.Name = seasonNumber == 0 ? diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index a693e3b26..951f439c2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -82,11 +82,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } } - else if (string.IsNullOrWhiteSpace(collectionType)) + else if (string.IsNullOrEmpty(collectionType)) { if (args.ContainsFileSystemEntryByName("tvshow.nfo")) { - if (args.Parent.IsRoot) + if (args.Parent != null && args.Parent.IsRoot) { // For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo return null; @@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } - if (args.Parent.IsRoot) + if (args.Parent != null && args.Parent.IsRoot) { return null; } @@ -160,11 +160,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return true; } - var allowOptimisticEpisodeDetection = isTvContentType; - var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(allowOptimisticEpisodeDetection); + var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); var episodeResolver = new Emby.Naming.TV.EpisodeResolver(namingOptions); - var episodeInfo = episodeResolver.Resolve(fullName, false, false); + bool? isNamed = null; + bool? isOptimistic = null; + + if (!isTvContentType) + { + isNamed = true; + isOptimistic = false; + } + + var episodeInfo = episodeResolver.Resolve(fullName, false, isNamed, isOptimistic, null, false); if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue) { return true; @@ -206,7 +214,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); - var seasonNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 8021399bd..7f04ac5bc 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -28,14 +28,14 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; _userManager = userManager; - _logger = logManager.GetLogger("Lucene"); + _logger = logManager.GetLogger("SearchEngine"); } - public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query) + public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (string.IsNullOrWhiteSpace(query.UserId)) + if (query.UserId.Equals(Guid.Empty)) { } else @@ -43,26 +43,22 @@ namespace Emby.Server.Implementations.Library user = _userManager.GetUserById(query.UserId); } - var results = await GetSearchHints(query, user).ConfigureAwait(false); - - var searchResultArray = results.ToArray(); - results = searchResultArray; - - var count = searchResultArray.Length; + var results = GetSearchHints(query, user); + var totalRecordCount = results.Count; if (query.StartIndex.HasValue) { - results = results.Skip(query.StartIndex.Value); + results = results.Skip(query.StartIndex.Value).ToList(); } if (query.Limit.HasValue) { - results = results.Take(query.Limit.Value); + results = results.Take(query.Limit.Value).ToList(); } return new QueryResult<SearchHintInfo> { - TotalRecordCount = count, + TotalRecordCount = totalRecordCount, Items = results.ToArray() }; @@ -83,24 +79,19 @@ namespace Emby.Server.Implementations.Library /// <param name="user">The user.</param> /// <returns>IEnumerable{SearchHintResult}.</returns> /// <exception cref="System.ArgumentNullException">searchTerm</exception> - private Task<IEnumerable<SearchHintInfo>> GetSearchHints(SearchQuery query, User user) + private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user) { var searchTerm = query.SearchTerm; - if (searchTerm != null) - { - searchTerm = searchTerm.Trim().RemoveDiacritics(); - } - - if (string.IsNullOrWhiteSpace(searchTerm)) + if (string.IsNullOrEmpty(searchTerm)) { throw new ArgumentNullException("searchTerm"); } - var terms = GetWords(searchTerm); + searchTerm = searchTerm.Trim().RemoveDiacritics(); var excludeItemTypes = query.ExcludeItemTypes.ToList(); - var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList(); + var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); excludeItemTypes.Add(typeof(Year).Name); excludeItemTypes.Add(typeof(Folder).Name); @@ -169,13 +160,13 @@ namespace Emby.Server.Implementations.Library var searchQuery = new InternalItemsQuery(user) { - NameContains = searchTerm, + SearchTerm = searchTerm, ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), Limit = query.Limit, - IncludeItemsByName = string.IsNullOrWhiteSpace(query.ParentId), - ParentId = string.IsNullOrWhiteSpace(query.ParentId) ? (Guid?)null : new Guid(query.ParentId), - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemsByName = string.IsNullOrEmpty(query.ParentId), + ParentId = string.IsNullOrEmpty(query.ParentId) ? Guid.Empty : new Guid(query.ParentId), + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, Recursive = true, IsKids = query.IsKids, @@ -201,120 +192,25 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase)) { - if (searchQuery.ParentId.HasValue) + if (!searchQuery.ParentId.Equals(Guid.Empty)) { - searchQuery.AncestorIds = new string[] { searchQuery.ParentId.Value.ToString("N") }; + searchQuery.AncestorIds = new[] { searchQuery.ParentId }; } - searchQuery.ParentId = null; + searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; - searchQuery.IncludeItemTypes = new string[] { }; - mediaItems = _libraryManager.GetArtists(searchQuery).Items.Select(i => i.Item1).ToList(); + searchQuery.IncludeItemTypes = Array.Empty<string>(); + mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList(); } else { mediaItems = _libraryManager.GetItemList(searchQuery); } - var returnValue = mediaItems.Select(item => - { - var index = GetIndex(item.Name, searchTerm, terms); - - return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2); - - }).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).Select(i => new SearchHintInfo - { - Item = i.Item1, - MatchedTerm = i.Item2 - }); - - return Task.FromResult(returnValue); - } - - /// <summary> - /// Gets the index. - /// </summary> - /// <param name="input">The input.</param> - /// <param name="searchInput">The search input.</param> - /// <param name="searchWords">The search input.</param> - /// <returns>System.Int32.</returns> - private Tuple<string, int> GetIndex(string input, string searchInput, List<string> searchWords) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentNullException("input"); - } - - input = input.RemoveDiacritics(); - - if (string.Equals(input, searchInput, StringComparison.OrdinalIgnoreCase)) - { - return new Tuple<string, int>(searchInput, 0); - } - - var index = input.IndexOf(searchInput, StringComparison.OrdinalIgnoreCase); - - if (index == 0) - { - return new Tuple<string, int>(searchInput, 1); - } - if (index > 0) - { - return new Tuple<string, int>(searchInput, 2); - } - - var items = GetWords(input); - - for (var i = 0; i < searchWords.Count; i++) + return mediaItems.Select(i => new SearchHintInfo { - var searchTerm = searchWords[i]; - - for (var j = 0; j < items.Count; j++) - { - var item = items[j]; - - if (string.Equals(item, searchTerm, StringComparison.OrdinalIgnoreCase)) - { - return new Tuple<string, int>(searchTerm, 3 + (i + 1) * (j + 1)); - } - - index = item.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase); - - if (index == 0) - { - return new Tuple<string, int>(searchTerm, 4 + (i + 1) * (j + 1)); - } - if (index > 0) - { - return new Tuple<string, int>(searchTerm, 5 + (i + 1) * (j + 1)); - } - } - } - return new Tuple<string, int>(null, -1); - } + Item = i - /// <summary> - /// Gets the words. - /// </summary> - /// <param name="term">The term.</param> - /// <returns>System.String[][].</returns> - private List<string> GetWords(string term) - { - var stoplist = GetStopList().ToList(); - - return term.Split() - .Where(i => !string.IsNullOrWhiteSpace(i) && !stoplist.Contains(i, StringComparer.OrdinalIgnoreCase)) - .ToList(); - } - - private IEnumerable<string> GetStopList() - { - return new[] - { - "the", - "a", - "of", - "an" - }; + }).ToList(); } } } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 7ef5ca35e..3714a7544 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -12,7 +12,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Querying; +using MediaBrowser.Controller.Dto; +using System.Globalization; namespace Emby.Server.Implementations.Library { @@ -29,10 +30,13 @@ namespace Emby.Server.Implementations.Library private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - public UserDataManager(ILogManager logManager, IServerConfigurationManager config) + private Func<IUserManager> _userManager; + + public UserDataManager(ILogManager logManager, IServerConfigurationManager config, Func<IUserManager> userManager) { _config = config; _logger = logManager.GetLogger(GetType().Name); + _userManager = userManager; } /// <summary> @@ -41,7 +45,14 @@ namespace Emby.Server.Implementations.Library /// <value>The repository.</value> public IUserDataRepository Repository { get; set; } - public void SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + { + var user = _userManager().GetUserById(userId); + + SaveUserData(user, item, userData, reason, cancellationToken); + } + + public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { if (userData == null) { @@ -51,15 +62,13 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException("item"); } - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } cancellationToken.ThrowIfCancellationRequested(); var keys = item.GetUserDataKeys(); + var userId = user.InternalId; + foreach (var key in keys) { Repository.SaveUserData(userId, key, userData, cancellationToken); @@ -73,7 +82,7 @@ namespace Emby.Server.Implementations.Library Keys = keys, UserData = userData, SaveReason = reason, - UserId = userId, + UserId = user.Id, Item = item }, _logger); @@ -88,18 +97,9 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) { - if (userData == null) - { - throw new ArgumentNullException("userData"); - } - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } + var user = _userManager().GetUserById(userId); - cancellationToken.ThrowIfCancellationRequested(); - - Repository.SaveAllUserData(userId, userData, cancellationToken); + Repository.SaveAllUserData(user.InternalId, userData, cancellationToken); } /// <summary> @@ -109,37 +109,30 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public List<UserItemData> GetAllUserData(Guid userId) { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } + var user = _userManager().GetUserById(userId); - return Repository.GetAllUserData(userId); + return Repository.GetAllUserData(user.InternalId); } public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } - if (keys == null) - { - throw new ArgumentNullException("keys"); - } - if (keys.Count == 0) - { - throw new ArgumentException("UserData keys cannot be empty."); - } + var user = _userManager().GetUserById(userId); + + return GetUserData(user, itemId, keys); + } + + public UserItemData GetUserData(User user, Guid itemId, List<string> keys) + { + var userId = user.InternalId; var cacheKey = GetCacheKey(userId, itemId); return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys)); } - private UserItemData GetUserDataInternal(Guid userId, List<string> keys) + private UserItemData GetUserDataInternal(long internalUserId, List<string> keys) { - var userData = Repository.GetUserData(userId, keys); + var userData = Repository.GetUserData(internalUserId, keys); if (userData != null) { @@ -150,7 +143,6 @@ namespace Emby.Server.Implementations.Library { return new UserItemData { - UserId = userId, Key = keys[0] }; } @@ -162,41 +154,41 @@ namespace Emby.Server.Implementations.Library /// Gets the internal key. /// </summary> /// <returns>System.String.</returns> - private string GetCacheKey(Guid userId, Guid itemId) + private string GetCacheKey(long internalUserId, Guid itemId) { - return userId.ToString("N") + itemId.ToString("N"); + return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N"); } - public UserItemData GetUserData(IHasUserData user, IHasUserData item) + public UserItemData GetUserData(User user, BaseItem item) { - return GetUserData(user.Id, item); + return GetUserData(user, item.Id, item.GetUserDataKeys()); } - public UserItemData GetUserData(string userId, IHasUserData item) + public UserItemData GetUserData(string userId, BaseItem item) { return GetUserData(new Guid(userId), item); } - public UserItemData GetUserData(Guid userId, IHasUserData item) + public UserItemData GetUserData(Guid userId, BaseItem item) { return GetUserData(userId, item.Id, item.GetUserDataKeys()); } - public UserItemDataDto GetUserDataDto(IHasUserData item, User user) + public UserItemDataDto GetUserDataDto(BaseItem item, User user) { - var userData = GetUserData(user.Id, item); + var userData = GetUserData(user, item); var dto = GetUserItemDataDto(userData); - item.FillUserDataDtoValues(dto, userData, null, user, new ItemFields[] { }); + item.FillUserDataDtoValues(dto, userData, null, user, new DtoOptions()); return dto; } - public UserItemDataDto GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, ItemFields[] fields) + public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options) { - var userData = GetUserData(user.Id, item); + var userData = GetUserData(user, item); var dto = GetUserItemDataDto(userData); - item.FillUserDataDtoValues(dto, userData, itemDto, user, fields); + item.FillUserDataDtoValues(dto, userData, itemDto, user, options); return dto; } @@ -230,13 +222,15 @@ namespace Emby.Server.Implementations.Library { var playedToCompletion = false; - var positionTicks = reportedPositionTicks ?? item.RunTimeTicks ?? 0; - var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0; + var runtimeTicks = item.GetRunTimeTicksForPlayState(); + + var positionTicks = reportedPositionTicks ?? runtimeTicks; + var hasRuntime = runtimeTicks > 0; // If a position has been reported, and if we know the duration if (positionTicks > 0 && hasRuntime) { - var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; + var pctIn = Decimal.Divide(positionTicks, runtimeTicks) * 100; // Don't track in very beginning if (pctIn < _config.Configuration.MinResumePct) @@ -245,7 +239,7 @@ namespace Emby.Server.Implementations.Library } // If we're at the end, assume completed - else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) { positionTicks = 0; data.Played = playedToCompletion = true; @@ -254,7 +248,7 @@ namespace Emby.Server.Implementations.Library else { // Enforce MinResumeDuration - var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; + var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) { diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 71c953b2c..b13a255aa 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -28,6 +28,11 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Plugins; namespace Emby.Server.Implementations.Library { @@ -40,7 +45,9 @@ namespace Emby.Server.Implementations.Library /// Gets the users. /// </summary> /// <value>The users.</value> - public IEnumerable<User> Users { get; private set; } + public IEnumerable<User> Users { get { return _users; } } + + private User[] _users; /// <summary> /// The _logger @@ -72,6 +79,9 @@ namespace Emby.Server.Implementations.Library private readonly IFileSystem _fileSystem; private readonly ICryptoProvider _cryptographyProvider; + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider) { _logger = logger; @@ -86,16 +96,38 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _cryptographyProvider = cryptographyProvider; ConfigurationManager = configurationManager; - Users = new List<User>(); + _users = Array.Empty<User>(); DeletePinFile(); } + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); + } + #region UserUpdated Event /// <summary> /// Occurs when [user updated]. /// </summary> public event EventHandler<GenericEventArgs<User>> UserUpdated; + public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; public event EventHandler<GenericEventArgs<User>> UserLockedOut; @@ -132,7 +164,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="System.ArgumentNullException"></exception> public User GetUserById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -162,7 +194,7 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - Users = LoadUsers(); + _users = LoadUsers(); var users = Users.ToList(); @@ -218,7 +250,7 @@ namespace Emby.Server.Implementations.Library return builder.ToString(); } - public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string passwordMd5, string remoteEndPoint, bool isUserSession) + public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) { if (string.IsNullOrWhiteSpace(username)) { @@ -229,18 +261,16 @@ namespace Emby.Server.Implementations.Library .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; + IAuthenticationProvider authenticationProvider = null; if (user != null) { - if (password != null) - { - hashedPassword = GetHashedString(user, password); - } - // Authenticate using local credentials if not a guest if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value != UserLinkType.Guest) { - success = AuthenticateLocalUser(user, password, hashedPassword, remoteEndPoint); + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; } // Maybe user accidently entered connect credentials. let's be flexible @@ -248,7 +278,7 @@ namespace Emby.Server.Implementations.Library { try { - await _connectFactory().Authenticate(user.ConnectUserName, password, passwordMd5).ConfigureAwait(false); + await _connectFactory().Authenticate(user.ConnectUserName, password).ConfigureAwait(false); success = true; } catch @@ -257,13 +287,43 @@ namespace Emby.Server.Implementations.Library } } } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } // Try originally entered username if (!success && (user == null || !string.Equals(user.ConnectUserName, username, StringComparison.OrdinalIgnoreCase))) { try { - var connectAuthResult = await _connectFactory().Authenticate(username, password, passwordMd5).ConfigureAwait(false); + var connectAuthResult = await _connectFactory().Authenticate(username, password).ConfigureAwait(false); user = Users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectAuthResult.User.Id, StringComparison.OrdinalIgnoreCase)); @@ -285,6 +345,19 @@ namespace Emby.Server.Implementations.Library throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); } + if (user != null) + { + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + } + // Update LastActivityDate and LastLoginDate, then save if (success) { @@ -305,34 +378,106 @@ namespace Emby.Server.Implementations.Library return success ? user : null; } - private bool AuthenticateLocalUser(User user, string password, string hashedPassword, string remoteEndPoint) + private string GetAuthenticationProviderId(IAuthenticationProvider provider) { - bool success; + return provider.GetType().FullName; + } - if (password == null) + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) { - // legacy - success = string.Equals(GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); } - else + + if (providers.Length == 0) { - success = string.Equals(GetPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; } - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + return providers; + } + + private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try { - if (password == null) + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); } else { - success = string.Equals(GetLocalPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.ErrorException("Error authenticating with provider {0}", ex, provider.Name); + + return false; + } + } + + private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } } } - return success; + if (user != null) + { + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + if (password == null) + { + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + } + } + } + + return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) @@ -367,63 +512,41 @@ namespace Emby.Server.Implementations.Library } } - private string GetPasswordHash(User user) - { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; - } - private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? GetEmptyHashedString(user) + ? _defaultAuthenticationProvider.GetEmptyHashedString(user) : user.EasyPassword; } private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - - private string GetEmptyHashedString(User user) - { - return GetHashedString(user, string.Empty); - } - - /// <summary> - /// Gets the hashed string. - /// </summary> - private string GetHashedString(User user, string str) - { - var salt = user.Salt; - if (salt != null) - { - // return BCrypt.HashPassword(str, salt); - } - - // legacy - return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); } /// <summary> /// Loads the users from the repository /// </summary> /// <returns>IEnumerable{User}.</returns> - private List<User> LoadUsers() + private User[] LoadUsers() { - var users = UserRepository.RetrieveAllUsers().ToList(); + var users = UserRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count == 0) { - var name = MakeValidUsername(Environment.UserName); + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyEmbyUser"; + } + var name = MakeValidUsername(defaultName); var user = InstantiateNewUser(name); user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.CreateUser(user); users.Add(user); @@ -433,7 +556,7 @@ namespace Emby.Server.Implementations.Library UpdateUserPolicy(user, user.Policy, false); } - return users; + return users.ToArray(); } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -443,9 +566,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException("user"); } - var passwordHash = GetPasswordHash(user); - - var hasConfiguredPassword = !IsPasswordEmpty(user, passwordHash); + var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? @@ -454,7 +575,7 @@ namespace Emby.Server.Implementations.Library var dto = new UserDto { - Id = user.Id.ToString("N"), + Id = user.Id, Name = user.Name, HasPassword = hasPassword, HasConfiguredPassword = hasConfiguredPassword, @@ -577,7 +698,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException("user"); } - if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) { throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); } @@ -585,7 +706,7 @@ namespace Emby.Server.Implementations.Library user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.UpdateUser(user); OnUserUpdated(user); } @@ -626,11 +747,11 @@ namespace Emby.Server.Implementations.Library var list = Users.ToList(); list.Add(user); - Users = list; + _users = list.ToArray(); user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.CreateUser(user); EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); @@ -658,7 +779,7 @@ namespace Emby.Server.Implementations.Library if (user.ConnectLinkType.HasValue) { - await _connectFactory().RemoveConnect(user.Id.ToString("N")).ConfigureAwait(false); + await _connectFactory().RemoveConnect(user).ConfigureAwait(false); } var allUsers = Users.ToList(); @@ -684,7 +805,7 @@ namespace Emby.Server.Implementations.Library { var configPath = GetConfigurationFilePath(user); - UserRepository.DeleteUser(user, CancellationToken.None); + UserRepository.DeleteUser(user); try { @@ -697,7 +818,7 @@ namespace Emby.Server.Implementations.Library DeleteUserPolicy(user); - Users = allUsers.Where(i => i.Id != user.Id).ToList(); + _users = allUsers.Where(i => i.Id != user.Id).ToArray(); OnUserDeleted(user); } @@ -711,9 +832,9 @@ namespace Emby.Server.Implementations.Library /// Resets the password by clearing it. /// </summary> /// <returns>Task.</returns> - public void ResetPassword(User user) + public Task ResetPassword(User user) { - ChangePassword(user, string.Empty, null); + return ChangePassword(user, string.Empty); } public void ResetEasyPassword(User user) @@ -721,29 +842,19 @@ namespace Emby.Server.Implementations.Library ChangeEasyPassword(user, string.Empty, null); } - public void ChangePassword(User user, string newPassword, string newPasswordHash) + public async Task ChangePassword(User user, string newPassword) { if (user == null) { throw new ArgumentNullException("user"); } - if (newPassword != null) - { - newPasswordHash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException("newPasswordHash"); - } - if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) { throw new ArgumentException("Passwords for guests cannot be changed."); } - user.Password = newPasswordHash; + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); UpdateUser(user); @@ -759,7 +870,7 @@ namespace Emby.Server.Implementations.Library if (newPassword != null) { - newPasswordHash = GetHashedString(user, newPassword); + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); } if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -801,7 +912,7 @@ namespace Emby.Server.Implementations.Library private PasswordPinCreationResult _lastPasswordPinCreationResult; private int _pinAttempts; - private PasswordPinCreationResult CreatePasswordResetPin() + private async Task<PasswordPinCreationResult> CreatePasswordResetPin() { var num = new Random().Next(1, 9999); @@ -815,7 +926,7 @@ namespace Emby.Server.Implementations.Library var text = new StringBuilder(); - var localAddress = _appHost.GetLocalApiUrl(CancellationToken.None).Result ?? string.Empty; + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; text.AppendLine("Use your web browser to visit:"); text.AppendLine(string.Empty); @@ -844,7 +955,7 @@ namespace Emby.Server.Implementations.Library return result; } - public ForgotPasswordResult StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { DeletePinFile(); @@ -872,7 +983,7 @@ namespace Emby.Server.Implementations.Library action = ForgotPasswordAction.PinCode; } - var result = CreatePasswordResetPin(); + var result = await CreatePasswordResetPin().ConfigureAwait(false); pinFile = result.PinFile; expirationDate = result.ExpirationDate; } @@ -885,7 +996,7 @@ namespace Emby.Server.Implementations.Library }; } - public PinRedeemResult RedeemPasswordResetPin(string pin) + public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) { DeletePinFile(); @@ -906,7 +1017,7 @@ namespace Emby.Server.Implementations.Library foreach (var user in users) { - ResetPassword(user); + await ResetPassword(user).ConfigureAwait(false); if (user.Policy.IsDisabled) { @@ -953,7 +1064,7 @@ namespace Emby.Server.Implementations.Library public UserPolicy GetUserPolicy(User user) { - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); try { @@ -988,7 +1099,7 @@ namespace Emby.Server.Implementations.Library } private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(string userId, UserPolicy userPolicy) + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); UpdateUserPolicy(user, userPolicy, true); @@ -1003,7 +1114,7 @@ namespace Emby.Server.Implementations.Library userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json); } - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); @@ -1013,12 +1124,15 @@ namespace Emby.Server.Implementations.Library user.Policy = userPolicy; } - UpdateConfiguration(user, user.Configuration, true); + if (fireEvent) + { + EventHelper.FireEventIfNotNull(UserPolicyUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + } } private void DeleteUserPolicy(User user) { - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); try { @@ -1037,7 +1151,7 @@ namespace Emby.Server.Implementations.Library } } - private string GetPolifyFilePath(User user) + private string GetPolicyFilePath(User user) { return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); } @@ -1075,9 +1189,14 @@ namespace Emby.Server.Implementations.Library } private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(string userId, UserConfiguration config) + 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); } @@ -1106,4 +1225,56 @@ namespace Emby.Server.Implementations.Library } } } + + 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 void Run() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + } + + 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() + { + + } + } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e97bf11c3..42f922710 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -39,24 +39,15 @@ namespace Emby.Server.Implementations.Library _config = config; } - public async Task<Folder[]> GetUserViews(UserViewQuery query, CancellationToken cancellationToken) + public Folder[] GetUserViews(UserViewQuery query) { var user = _userManager.GetUserById(query.UserId); - var folders = user.RootFolder + var folders = _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType<Folder>() .ToList(); - if (!query.IncludeHidden) - { - folders = folders.Where(i => - { - var hidden = i as IHiddenFromDisplay; - return hidden == null || !hidden.IsHiddenFromUser(user); - }).ToList(); - } - var groupedFolders = new List<ICollectionFolder>(); var list = new List<Folder>(); @@ -68,7 +59,7 @@ namespace Emby.Server.Implementations.Library if (UserView.IsUserSpecific(folder)) { - list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken)); + list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null)); continue; } @@ -80,7 +71,7 @@ namespace Emby.Server.Implementations.Library if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - list.Add(GetUserView(folder, folderViewType, string.Empty, cancellationToken)); + list.Add(GetUserView(folder, folderViewType, string.Empty)); } else { @@ -90,7 +81,7 @@ namespace Emby.Server.Implementations.Library foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) { - var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType)) + var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType)) .ToList(); if (parents.Count > 0) @@ -99,41 +90,38 @@ namespace Emby.Server.Implementations.Library "TvShows" : "Movies"; - list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews, cancellationToken)); + list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews)); } } if (_config.Configuration.EnableFolderView) { var name = _localizationManager.GetLocalizedString("Folders"); - list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken)); + list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty)); } if (query.IncludeExternalContent) { - var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery + var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery { UserId = query.UserId - - }, cancellationToken).ConfigureAwait(false); + }); var channels = channelResult.Items; - if (_config.Configuration.EnableChannelView && channels.Length > 0) - { - list.Add(_channelManager.GetInternalChannelFolder(cancellationToken)); - } - else - { - list.AddRange(channels); - } + list.AddRange(channels); - if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId)) + if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(query.UserId)) { list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None)); } } + if (!query.IncludeHidden) + { + list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N"))).ToList(); + } + var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); var orders = user.Configuration.OrderedViews.ToList(); @@ -148,7 +136,7 @@ namespace Emby.Server.Implementations.Library var view = i as UserView; if (view != null) { - if (view.DisplayParentId != Guid.Empty) + if (!view.DisplayParentId.Equals(Guid.Empty)) { index = orders.IndexOf(view.DisplayParentId.ToString("N")); } @@ -162,21 +150,21 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public UserView GetUserSubViewWithName(string name, string parentId, string type, string sortName, CancellationToken cancellationToken) + public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName) { var uniqueId = parentId + "subview" + type; - return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId, cancellationToken); + return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId); } - public UserView GetUserSubView(string parentId, string type, string localizationKey, string sortName, CancellationToken cancellationToken) + public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName) { var name = _localizationManager.GetLocalizedString(localizationKey); - return GetUserSubViewWithName(name, parentId, type, sortName, cancellationToken); + return GetUserSubViewWithName(name, parentId, type, sortName); } - private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews, CancellationToken cancellationToken) + private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews) { if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) { @@ -185,16 +173,16 @@ namespace Emby.Server.Implementations.Library return (Folder)parents[0]; } - return GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken); + return GetUserView((Folder)parents[0], viewType, string.Empty); } var name = _localizationManager.GetLocalizedString(localizationKey); - return _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken); + return _libraryManager.GetNamedView(user, name, viewType, sortName); } - public UserView GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken) + public UserView GetUserView(Folder parent, string viewType, string sortName) { - return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken); + return _libraryManager.GetShadowView(parent, viewType, sortName); } public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options) @@ -246,9 +234,26 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!string.IsNullOrWhiteSpace(parentId)) + if (!parentId.Equals(Guid.Empty)) { - var parent = _libraryManager.GetItemById(parentId) as Folder; + var parentItem = _libraryManager.GetItemById(parentId); + var parentItemChannel = parentItem as Channel; + if (parentItemChannel != null) + { + return _channelManager.GetLatestChannelItemsInternal(new InternalItemsQuery(user) + { + ChannelIds = new [] { parentId }, + IsPlayed = request.IsPlayed, + StartIndex = request.StartIndex, + Limit = request.Limit, + IncludeItemTypes = request.IncludeItemTypes, + EnableTotalRecordCount = false + + + }, CancellationToken.None).Result.Items.ToList(); + } + + var parent = parentItem as Folder; if (parent != null) { parents.Add(parent); @@ -264,7 +269,7 @@ namespace Emby.Server.Implementations.Library if (parents.Count == 0) { - parents = user.RootFolder.GetChildren(user, true) + parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) .ToList(); @@ -275,6 +280,24 @@ namespace Emby.Server.Implementations.Library return new List<BaseItem>(); } + if (includeItemTypes.Length == 0) + { + // Handle situations with the grouping setting, e.g. movies showing up in tv, etc. + // Thanks to mixed content libraries included in the UserView + var hasCollectionType = parents.OfType<UserView>().ToArray(); + if (hasCollectionType.Length > 0) + { + if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))) + { + includeItemTypes = new string[] { "Movie" }; + } + else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))) + { + includeItemTypes = new string[] { "Episode" }; + } + } + } + var mediaTypes = new List<string>(); if (includeItemTypes.Length == 0) @@ -285,6 +308,7 @@ namespace Emby.Server.Implementations.Library { case CollectionType.Books: mediaTypes.Add(MediaType.Book); + mediaTypes.Add(MediaType.Audio); break; case CollectionType.Games: mediaTypes.Add(MediaType.Game); @@ -318,12 +342,12 @@ namespace Emby.Server.Implementations.Library typeof(MusicGenre).Name, typeof(Genre).Name - } : new string[] { }; + } : Array.Empty<string>(); var query = new InternalItemsQuery(user) { IncludeItemTypes = includeItemTypes, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null, ExcludeItemTypes = excludeItemTypes, IsVirtualItem = false, diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 1a53ad672..cd2aab4c8 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -81,33 +81,27 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(percent); } - names = names.Select(i => i.RemoveDiacritics()).DistinctNames().ToList(); - - var artistEntities = _libraryManager.GetItemList(new InternalItemsQuery + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(MusicArtist).Name } - + IncludeItemTypes = new[] { typeof(MusicArtist).Name }, + IsDeadArtist = true, + IsLocked = false }).Cast<MusicArtist>().ToList(); - foreach (var artist in artistEntities) + foreach (var item in deadEntities) { - if (!artist.IsAccessedByName) + if (!item.IsAccessedByName) { continue; } + + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); - var name = (artist.Name ?? string.Empty).RemoveDiacritics(); - - if (!names.Contains(name, StringComparer.OrdinalIgnoreCase)) + _libraryManager.DeleteItem(item, new DeleteOptions { - _logger.Info("Deleting dead artist {0} {1}.", artist.Id.ToString("N"), artist.Name); + DeleteFileLocation = false - await _libraryManager.DeleteItem(artist, new DeleteOptions - { - DeleteFileLocation = false - - }).ConfigureAwait(false); - } + }, false); } progress.Report(100); diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 39630cf96..1f4e1de92 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,18 +1,11 @@ -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Validators @@ -73,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Validators var options = new MetadataRefreshOptions(_fileSystem) { - ImageRefreshMode = ImageRefreshMode.ValidationOnly, + ImageRefreshMode = MetadataRefreshMode.ValidationOnly, MetadataRefreshMode = MetadataRefreshMode.ValidationOnly }; @@ -96,6 +89,23 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(100 * percent); } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Person).Name }, + IsDeadPerson = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + }, false); + } + progress.Report(100); _logger.Info("People validation complete"); diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 97b8ff0ac..f306309b3 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -68,6 +68,24 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(percent); } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Studio).Name }, + IsDeadStudio = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + + }, false); + } + progress.Report(100); } } |
