From 48facb797ed912e4ea6b04b17d1ff190ac2daac4 Mon Sep 17 00:00:00 2001 From: stefan Date: Wed, 12 Sep 2018 19:26:21 +0200 Subject: Update to 3.5.2 and .net core 2.1 --- .../Library/CoreResolutionIgnoreRule.cs | 88 ++- .../Library/DefaultAuthenticationProvider.cs | 105 ++++ .../Library/ExclusiveLiveStream.cs | 42 ++ .../Library/LibraryManager.cs | 700 ++++++++++----------- .../Library/LiveStreamHelper.cs | 181 ++++++ .../Library/LocalTrailerPostScanTask.cs | 105 ---- .../Library/MediaSourceManager.cs | 599 ++++++++++++++---- .../Library/MediaStreamSelector.cs | 217 +++++++ .../Library/MusicManager.cs | 12 +- .../Library/ResolverHelper.cs | 24 +- .../Library/Resolvers/Audio/AudioResolver.cs | 8 +- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 33 +- .../Library/Resolvers/BaseVideoResolver.cs | 5 - .../Library/Resolvers/Movies/BoxSetResolver.cs | 8 +- .../Library/Resolvers/Movies/MovieResolver.cs | 36 +- .../Library/Resolvers/PhotoResolver.cs | 3 +- .../Library/Resolvers/PlaylistResolver.cs | 27 +- .../Library/Resolvers/TV/SeasonResolver.cs | 16 +- .../Library/Resolvers/TV/SeriesResolver.cs | 22 +- .../Library/SearchEngine.cs | 152 +---- .../Library/UserDataManager.cs | 106 ++-- Emby.Server.Implementations/Library/UserManager.cs | 369 ++++++++--- .../Library/UserViewManager.cs | 110 ++-- .../Library/Validators/ArtistsValidator.cs | 28 +- .../Library/Validators/PeopleValidator.cs | 28 +- .../Library/Validators/StudiosValidator.cs | 18 + 26 files changed, 1994 insertions(+), 1048 deletions(-) create mode 100644 Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs create mode 100644 Emby.Server.Implementations/Library/ExclusiveLiveStream.cs create mode 100644 Emby.Server.Implementations/Library/LiveStreamHelper.cs delete mode 100644 Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/MediaStreamSelector.cs (limited to 'Emby.Server.Implementations/Library') 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; + /// /// Any folder named in this list will be ignored - can be added to at runtime for extensibility /// - public static readonly List IgnoreFolders = new List + public static readonly Dictionary IgnoreFolders = new List { "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; } /// @@ -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 Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + public Task Authenticate(string username, string password, User resolvedUser) + { + if (resolvedUser == null) + { + throw new Exception("Invalid username or password"); + } + + var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + public Task 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); + } + + /// + /// Gets the hashed string. + /// + 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 _closeFn; + + public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func 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 { @@ -70,12 +73,6 @@ namespace Emby.Server.Implementations.Library /// The entity resolution ignore rules. private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } - /// - /// Gets the list of BasePluginFolders added by plugins - /// - /// The plugin folders. - private IVirtualFolderCreator[] PluginFolderCreators { get; set; } - /// /// Gets the list of currently registered entity resolvers /// @@ -140,6 +137,7 @@ namespace Emby.Server.Implementations.Library private readonly Func _providerManagerFactory; private readonly Func _userviewManager; public bool IsScanRunning { get; private set; } + private IServerApplicationHost _appHost; /// /// The _library items cache @@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Library /// The user manager. /// The configuration manager. /// The user data repository. - public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func libraryMonitorFactory, IFileSystem fileSystem, Func providerManagerFactory, Func userviewManager) + public LibraryManager(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func libraryMonitorFactory, IFileSystem fileSystem, Func providerManagerFactory, Func 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(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -195,14 +194,12 @@ namespace Emby.Server.Implementations.Library /// The item comparers. /// The postscan tasks. public void AddParts(IEnumerable rules, - IEnumerable pluginFolders, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, IEnumerable postscanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); - PluginFolderCreators = pluginFolders.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType().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(); @@ -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(), 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(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new ValueTuple(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(); - return Task.FromResult(true); + return Task.CompletedTask; } /// @@ -1031,48 +1057,45 @@ namespace Emby.Server.Implementations.Library } } - private async Task PerformLibraryValidation(IProgress 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(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false); - progress.Report(1); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); - progress.Report(2); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType().ToList()) { await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); } - progress.Report(3); + } + + private async Task PerformLibraryValidation(IProgress progress, CancellationToken cancellationToken) + { + _logger.Info("Validating media library"); + + await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false); var innerProgress = new ActionableProgress(); - 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(); - 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)); } /// @@ -1208,7 +1248,7 @@ namespace Emby.Server.Implementations.Library /// id 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 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 { 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 { 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(); // 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 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 { 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(); } - 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(); } // 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() - .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(); } var collectionFolder = item as CollectionFolder; @@ -1582,7 +1611,7 @@ namespace Emby.Server.Implementations.Library { return new[] { topParent.Id }; } - return new Guid[] { }; + return Array.Empty(); } /// @@ -1737,7 +1766,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderByList) + public IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderByList) { var isFirst = true; @@ -1802,9 +1831,9 @@ namespace Emby.Server.Implementations.Library /// The item. /// The cancellation token. /// Task. - 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); } /// @@ -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,45 +1889,64 @@ namespace Emby.Server.Implementations.Library /// /// Updates the item. /// - /// The item. - /// The update reason. - /// The cancellation token. - /// Task. - public void UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken) + public void UpdateItems(List 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); + } } } } + /// + /// Updates the item. + /// + /// The item. + /// The update reason. + /// The cancellation token. + /// Task. + public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + { + UpdateItems(new List { item }, parent, updateReason, cancellationToken); + } + /// /// Reports the item removed. /// @@ -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() .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 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.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() : 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 ConvertImageToLocal(IHasMetadata item, ItemImageInfo image, int imageIndex) + public async Task 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()); } 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(), 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(), 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(), 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(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(); + 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 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().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 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 _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 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 providers) @@ -109,20 +121,23 @@ namespace Emby.Server.Implementations.Library return streams; } - public async Task> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken) + public async Task> 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(); @@ -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> 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> 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> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken) + private async Task> 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 GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) + public async Task 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 GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) + public List 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(); } 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() : new List { user.Configuration.SubtitleLanguagePreference }; + ? Array.Empty() : 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() + : 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 SortMediaSources(IEnumerable sources) { return sources.OrderBy(i => @@ -352,55 +421,157 @@ namespace Emby.Server.Implementations.Library .ToList(); } - private readonly Dictionary _openStreams = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _openStreams = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + public async Task> 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(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(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(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 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 OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + { + var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false); + return result.Item1; + } + + public async Task 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(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(); + 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> 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(info.MediaSource, info as IDirectStreamProvider); + } + + private async Task 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(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 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 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 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 GetSortedStreams(IEnumerable 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 streams, + string[] preferredLanguages, + SubtitlePlaybackMode mode, + string audioTrackLanguage) + { + if (mode == SubtitlePlaybackMode.None) + { + return; + } + + streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) + .ToList(); + + var filteredStreams = new List(); + + 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(); + + 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 GetInstantMixFromGenreIds(IEnumerable genreIds, User user, DtoOptions dtoOptions) + public List 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(ItemSortBy.Random, SortOrder.Ascending) }, + OrderBy = new [] { new ValueTuple(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"); } @@ -107,17 +107,6 @@ namespace Emby.Server.Implementations.Library return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); } - /// - /// The MB name regex - /// - 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+", " "); - } - /// /// Ensures DateCreated and DateModified have values /// @@ -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 /// MusicAlbum. protected override MusicAlbum Resolve(ItemResolveArgs args) { - if (!args.IsDirectory) return null; - - // Avoid mis-identifying top folders - if (args.HasParent()) 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()) 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