|  | @@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |      /// </summary>
 | 
	
		
			
				|  |  |      public class LibraryManager : ILibraryManager
 | 
	
		
			
				|  |  |      {
 | 
	
		
			
				|  |  | +        private const string ShortcutFileExtension = ".mblink";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          private readonly ILogger<LibraryManager> _logger;
 | 
	
		
			
				|  |  |          private readonly ITaskManager _taskManager;
 | 
	
		
			
				|  |  |          private readonly IUserManager _userManager;
 | 
	
	
		
			
				|  | @@ -75,68 +77,29 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |          private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
 | 
	
		
			
				|  |  |          private readonly IImageProcessor _imageProcessor;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        private NamingOptions _namingOptions;
 | 
	
		
			
				|  |  | -        private string[] _videoFileExtensions;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Gets or sets the postscan tasks.
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        /// <value>The postscan tasks.</value>
 | 
	
		
			
				|  |  | -        private ILibraryPostScanTask[] PostscanTasks { get; set; }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Gets or sets the intro providers.
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        /// <value>The intro providers.</value>
 | 
	
		
			
				|  |  | -        private IIntroProvider[] IntroProviders { get; set; }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Gets or sets the list of entity resolution ignore rules.
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        /// <value>The entity resolution ignore rules.</value>
 | 
	
		
			
				|  |  | -        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// Gets or sets the list of currently registered entity resolvers.
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        /// <value>The entity resolvers enumerable.</value>
 | 
	
		
			
				|  |  | -        private IItemResolver[] EntityResolvers { get; set; }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        private IMultiItemResolver[] MultiItemResolvers { get; set; }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Gets or sets the comparers.
 | 
	
		
			
				|  |  | +        /// The _root folder sync lock.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        /// <value>The comparers.</value>
 | 
	
		
			
				|  |  | -        private IBaseItemComparer[] Comparers { get; set; }
 | 
	
		
			
				|  |  | +        private readonly object _rootFolderSyncLock = new object();
 | 
	
		
			
				|  |  | +        private readonly object _userRootFolderSyncLock = new object();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Occurs when [item added].
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        public event EventHandler<ItemChangeEventArgs> ItemAdded;
 | 
	
		
			
				|  |  | +        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Occurs when [item updated].
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 | 
	
		
			
				|  |  | +        private NamingOptions _namingOptions;
 | 
	
		
			
				|  |  | +        private string[] _videoFileExtensions;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// Occurs when [item removed].
 | 
	
		
			
				|  |  | +        /// The _root folder.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 | 
	
		
			
				|  |  | +        private volatile AggregateFolder _rootFolder;
 | 
	
		
			
				|  |  | +        private volatile UserRootFolder _userRootFolder;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        public bool IsScanRunning { get; private set; }
 | 
	
		
			
				|  |  | +        private bool _wizardCompleted;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  |          /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        /// <param name="appHost">The application host</param>
 | 
	
		
			
				|  |  | +        /// <param name="appHost">The application host.</param>
 | 
	
		
			
				|  |  |          /// <param name="logger">The logger.</param>
 | 
	
		
			
				|  |  |          /// <param name="taskManager">The task manager.</param>
 | 
	
		
			
				|  |  |          /// <param name="userManager">The user manager.</param>
 | 
	
	
		
			
				|  | @@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// Adds the parts.
 | 
	
		
			
				|  |  | +        /// Occurs when [item added].
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        /// <param name="rules">The rules.</param>
 | 
	
		
			
				|  |  | -        /// <param name="resolvers">The resolvers.</param>
 | 
	
		
			
				|  |  | -        /// <param name="introProviders">The intro providers.</param>
 | 
	
		
			
				|  |  | -        /// <param name="itemComparers">The item comparers.</param>
 | 
	
		
			
				|  |  | -        /// <param name="postscanTasks">The post scan tasks.</param>
 | 
	
		
			
				|  |  | -        public void AddParts(
 | 
	
		
			
				|  |  | -            IEnumerable<IResolverIgnoreRule> rules,
 | 
	
		
			
				|  |  | -            IEnumerable<IItemResolver> resolvers,
 | 
	
		
			
				|  |  | -            IEnumerable<IIntroProvider> introProviders,
 | 
	
		
			
				|  |  | -            IEnumerable<IBaseItemComparer> itemComparers,
 | 
	
		
			
				|  |  | -            IEnumerable<ILibraryPostScanTask> postscanTasks)
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            EntityResolutionIgnoreRules = rules.ToArray();
 | 
	
		
			
				|  |  | -            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 | 
	
		
			
				|  |  | -            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 | 
	
		
			
				|  |  | -            IntroProviders = introProviders.ToArray();
 | 
	
		
			
				|  |  | -            Comparers = itemComparers.ToArray();
 | 
	
		
			
				|  |  | -            PostscanTasks = postscanTasks.ToArray();
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | +        public event EventHandler<ItemChangeEventArgs> ItemAdded;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// The _root folder.
 | 
	
		
			
				|  |  | +        /// Occurs when [item updated].
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        private volatile AggregateFolder _rootFolder;
 | 
	
		
			
				|  |  | +        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// The _root folder sync lock.
 | 
	
		
			
				|  |  | +        /// Occurs when [item removed].
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        private readonly object _rootFolderSyncLock = new object();
 | 
	
		
			
				|  |  | +        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  |          /// Gets the root folder.
 | 
	
	
		
			
				|  | @@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        private bool _wizardCompleted;
 | 
	
		
			
				|  |  | +        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets or sets the postscan tasks.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <value>The postscan tasks.</value>
 | 
	
		
			
				|  |  | +        private ILibraryPostScanTask[] PostscanTasks { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets or sets the intro providers.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <value>The intro providers.</value>
 | 
	
		
			
				|  |  | +        private IIntroProvider[] IntroProviders { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets or sets the list of entity resolution ignore rules.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <value>The entity resolution ignore rules.</value>
 | 
	
		
			
				|  |  | +        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets or sets the list of currently registered entity resolvers.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <value>The entity resolvers enumerable.</value>
 | 
	
		
			
				|  |  | +        private IItemResolver[] EntityResolvers { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private IMultiItemResolver[] MultiItemResolvers { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Gets or sets the comparers.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <value>The comparers.</value>
 | 
	
		
			
				|  |  | +        private IBaseItemComparer[] Comparers { get; set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        public bool IsScanRunning { get; private set; }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        /// <summary>
 | 
	
		
			
				|  |  | +        /// Adds the parts.
 | 
	
		
			
				|  |  | +        /// </summary>
 | 
	
		
			
				|  |  | +        /// <param name="rules">The rules.</param>
 | 
	
		
			
				|  |  | +        /// <param name="resolvers">The resolvers.</param>
 | 
	
		
			
				|  |  | +        /// <param name="introProviders">The intro providers.</param>
 | 
	
		
			
				|  |  | +        /// <param name="itemComparers">The item comparers.</param>
 | 
	
		
			
				|  |  | +        /// <param name="postscanTasks">The post scan tasks.</param>
 | 
	
		
			
				|  |  | +        public void AddParts(
 | 
	
		
			
				|  |  | +            IEnumerable<IResolverIgnoreRule> rules,
 | 
	
		
			
				|  |  | +            IEnumerable<IItemResolver> resolvers,
 | 
	
		
			
				|  |  | +            IEnumerable<IIntroProvider> introProviders,
 | 
	
		
			
				|  |  | +            IEnumerable<IBaseItemComparer> itemComparers,
 | 
	
		
			
				|  |  | +            IEnumerable<ILibraryPostScanTask> postscanTasks)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            EntityResolutionIgnoreRules = rules.ToArray();
 | 
	
		
			
				|  |  | +            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 | 
	
		
			
				|  |  | +            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 | 
	
		
			
				|  |  | +            IntroProviders = introProviders.ToArray();
 | 
	
		
			
				|  |  | +            Comparers = itemComparers.ToArray();
 | 
	
		
			
				|  |  | +            PostscanTasks = postscanTasks.ToArray();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  |          /// Records the configuration values.
 | 
	
	
		
			
				|  | @@ -341,7 +347,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              if (item is LiveTvProgram)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  _logger.LogDebug(
 | 
	
		
			
				|  |  | -                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  | +                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  |                      item.GetType().Name,
 | 
	
		
			
				|  |  |                      item.Name ?? "Unknown name",
 | 
	
		
			
				|  |  |                      item.Path ?? string.Empty,
 | 
	
	
		
			
				|  | @@ -350,7 +356,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              else
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  _logger.LogInformation(
 | 
	
		
			
				|  |  | -                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  | +                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  |                      item.GetType().Name,
 | 
	
		
			
				|  |  |                      item.Name ?? "Unknown name",
 | 
	
		
			
				|  |  |                      item.Path ?? string.Empty,
 | 
	
	
		
			
				|  | @@ -368,7 +374,12 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                      continue;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
 | 
	
		
			
				|  |  | +                _logger.LogDebug(
 | 
	
		
			
				|  |  | +                    "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  | +                    item.GetType().Name,
 | 
	
		
			
				|  |  | +                    item.Name ?? "Unknown name",
 | 
	
		
			
				|  |  | +                    metadataPath,
 | 
	
		
			
				|  |  | +                    item.Id);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  try
 | 
	
		
			
				|  |  |                  {
 | 
	
	
		
			
				|  | @@ -392,7 +403,13 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                      {
 | 
	
		
			
				|  |  |                          try
 | 
	
		
			
				|  |  |                          {
 | 
	
		
			
				|  |  | -                            _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
 | 
	
		
			
				|  |  | +                            _logger.LogInformation(
 | 
	
		
			
				|  |  | +                                "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
 | 
	
		
			
				|  |  | +                                item.GetType().Name,
 | 
	
		
			
				|  |  | +                                item.Name ?? "Unknown name",
 | 
	
		
			
				|  |  | +                                fileSystemInfo.FullName,
 | 
	
		
			
				|  |  | +                                item.Id);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                              if (fileSystemInfo.IsDirectory)
 | 
	
		
			
				|  |  |                              {
 | 
	
		
			
				|  |  |                                  Directory.Delete(fileSystemInfo.FullName, true);
 | 
	
	
		
			
				|  | @@ -500,8 +517,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  // Try to normalize paths located underneath program-data in an attempt to make them more portable
 | 
	
		
			
				|  |  |                  key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
 | 
	
		
			
				|  |  | -                    .TrimStart(new[] { '/', '\\' })
 | 
	
		
			
				|  |  | -                    .Replace("/", "\\");
 | 
	
		
			
				|  |  | +                    .TrimStart('/', '\\')
 | 
	
		
			
				|  |  | +                    .Replace('/', '\\');
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 | 
	
	
		
			
				|  | @@ -514,8 +531,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              return key.GetMD5();
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, bool allowIgnorePath = true)
 | 
	
		
			
				|  |  | -            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent, allowIgnorePath: allowIgnorePath);
 | 
	
		
			
				|  |  | +        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
 | 
	
		
			
				|  |  | +            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          private BaseItem ResolvePath(
 | 
	
		
			
				|  |  |              FileSystemMetadata fileInfo,
 | 
	
	
		
			
				|  | @@ -523,8 +540,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              IItemResolver[] resolvers,
 | 
	
		
			
				|  |  |              Folder parent = null,
 | 
	
		
			
				|  |  |              string collectionType = null,
 | 
	
		
			
				|  |  | -            LibraryOptions libraryOptions = null,
 | 
	
		
			
				|  |  | -            bool allowIgnorePath = true)
 | 
	
		
			
				|  |  | +            LibraryOptions libraryOptions = null)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              if (fileInfo == null)
 | 
	
		
			
				|  |  |              {
 | 
	
	
		
			
				|  | @@ -548,7 +564,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // Return null if ignore rules deem that we should do so
 | 
	
		
			
				|  |  | -            if (allowIgnorePath && IgnoreFile(args.FileInfo, args.Parent))
 | 
	
		
			
				|  |  | +            if (IgnoreFile(args.FileInfo, args.Parent))
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  return null;
 | 
	
		
			
				|  |  |              }
 | 
	
	
		
			
				|  | @@ -713,7 +729,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              Directory.CreateDirectory(rootFolderPath);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 | 
	
		
			
				|  |  | -                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false))
 | 
	
		
			
				|  |  | +                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
 | 
	
		
			
				|  |  |                               .DeepCopy<Folder, AggregateFolder>();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // In case program data folder was moved
 | 
	
	
		
			
				|  | @@ -765,14 +781,11 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              return rootFolder;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        private volatile UserRootFolder _userRootFolder;
 | 
	
		
			
				|  |  | -        private readonly object _syncLock = new object();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          public Folder GetUserRootFolder()
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              if (_userRootFolder == null)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                lock (_syncLock)
 | 
	
		
			
				|  |  | +                lock (_userRootFolderSyncLock)
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      if (_userRootFolder == null)
 | 
	
		
			
				|  |  |                      {
 | 
	
	
		
			
				|  | @@ -795,7 +808,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                          if (tmpItem == null)
 | 
	
		
			
				|  |  |                          {
 | 
	
		
			
				|  |  |                              _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 | 
	
		
			
				|  |  | -                            tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath), allowIgnorePath: false)).DeepCopy<Folder, UserRootFolder>();
 | 
	
		
			
				|  |  | +                            tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
 | 
	
		
			
				|  |  |                          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                          // In case program data folder was moved
 | 
	
	
		
			
				|  | @@ -1322,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              return new QueryResult<BaseItem>
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                Items = _itemRepository.GetItemList(query).ToArray()
 | 
	
		
			
				|  |  | +                Items = _itemRepository.GetItemList(query)
 | 
	
		
			
				|  |  |              };
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -1453,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  return _itemRepository.GetItems(query);
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            var list = _itemRepository.GetItemList(query);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |              return new QueryResult<BaseItem>
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                Items = list
 | 
	
		
			
				|  |  | +                Items = _itemRepository.GetItemList(query)
 | 
	
		
			
				|  |  |              };
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -1793,7 +1804,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// Creates the items.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  |          /// <param name="items">The items.</param>
 | 
	
		
			
				|  |  | -        /// <param name="parent">The parent item</param>
 | 
	
		
			
				|  |  | +        /// <param name="parent">The parent item.</param>
 | 
	
		
			
				|  |  |          /// <param name="cancellationToken">The cancellation token.</param>
 | 
	
		
			
				|  |  |          public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  |          {
 | 
	
	
		
			
				|  | @@ -1866,7 +1877,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 | 
	
		
			
				|  |  | -            if (outdated.Length == 0)
 | 
	
		
			
				|  |  | +            // Skip image processing if current or live tv source
 | 
	
		
			
				|  |  | +            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  RegisterItem(item);
 | 
	
		
			
				|  |  |                  return;
 | 
	
	
		
			
				|  | @@ -1894,9 +1906,19 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                      }
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
 | 
	
		
			
				|  |  | -                image.Width = size.Width;
 | 
	
		
			
				|  |  | -                image.Height = size.Height;
 | 
	
		
			
				|  |  | +                try
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
 | 
	
		
			
				|  |  | +                    image.Width = size.Width;
 | 
	
		
			
				|  |  | +                    image.Height = size.Height;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                catch (Exception ex)
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
 | 
	
		
			
				|  |  | +                    image.Width = 0;
 | 
	
		
			
				|  |  | +                    image.Height = 0;
 | 
	
		
			
				|  |  | +                    continue;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  try
 | 
	
		
			
				|  |  |                  {
 | 
	
	
		
			
				|  | @@ -1925,12 +1947,9 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  |          /// Updates the item.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  | +        public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  | -            // Don't iterate multiple times
 | 
	
		
			
				|  |  | -            var itemsList = items.ToList();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -            foreach (var item in itemsList)
 | 
	
		
			
				|  |  | +            foreach (var item in items)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  if (item.IsFileProtocol)
 | 
	
		
			
				|  |  |                  {
 | 
	
	
		
			
				|  | @@ -1942,11 +1961,11 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            _itemRepository.SaveItems(itemsList, cancellationToken);
 | 
	
		
			
				|  |  | +            _itemRepository.SaveItems(items, cancellationToken);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (ItemUpdated != null)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                foreach (var item in itemsList)
 | 
	
		
			
				|  |  | +                foreach (var item in items)
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      // With the live tv guide this just creates too much noise
 | 
	
		
			
				|  |  |                      if (item.SourceType != SourceType.Library)
 | 
	
	
		
			
				|  | @@ -2169,8 +2188,6 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          public UserView GetNamedView(
 | 
	
		
			
				|  |  |              User user,
 | 
	
		
			
				|  |  |              string name,
 | 
	
	
		
			
				|  | @@ -2468,14 +2485,9 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            var episodeInfo = episode.IsFileProtocol ?
 | 
	
		
			
				|  |  | -                resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
 | 
	
		
			
				|  |  | -                new Naming.TV.EpisodeInfo();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -            if (episodeInfo == null)
 | 
	
		
			
				|  |  | -            {
 | 
	
		
			
				|  |  | -                episodeInfo = new Naming.TV.EpisodeInfo();
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | +            var episodeInfo = episode.IsFileProtocol
 | 
	
		
			
				|  |  | +                ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
 | 
	
		
			
				|  |  | +                : new Naming.TV.EpisodeInfo();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              try
 | 
	
		
			
				|  |  |              {
 | 
	
	
		
			
				|  | @@ -2483,11 +2495,13 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      // Read from metadata
 | 
	
		
			
				|  |  | -                    var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
 | 
	
		
			
				|  |  | -                    {
 | 
	
		
			
				|  |  | -                        MediaSource = episode.GetMediaSources(false)[0],
 | 
	
		
			
				|  |  | -                        MediaType = DlnaProfileType.Video
 | 
	
		
			
				|  |  | -                    }, CancellationToken.None).GetAwaiter().GetResult();
 | 
	
		
			
				|  |  | +                    var mediaInfo = _mediaEncoder.GetMediaInfo(
 | 
	
		
			
				|  |  | +                        new MediaInfoRequest
 | 
	
		
			
				|  |  | +                        {
 | 
	
		
			
				|  |  | +                            MediaSource = episode.GetMediaSources(false)[0],
 | 
	
		
			
				|  |  | +                            MediaType = DlnaProfileType.Video
 | 
	
		
			
				|  |  | +                        },
 | 
	
		
			
				|  |  | +                        CancellationToken.None).GetAwaiter().GetResult();
 | 
	
		
			
				|  |  |                      if (mediaInfo.ParentIndexNumber > 0)
 | 
	
		
			
				|  |  |                      {
 | 
	
		
			
				|  |  |                          episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
 | 
	
	
		
			
				|  | @@ -2645,7 +2659,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var videos = videoListResolver.Resolve(fileSystemChildren);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
 | 
	
		
			
				|  |  | +            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (currentVideo != null)
 | 
	
		
			
				|  |  |              {
 | 
	
	
		
			
				|  | @@ -2662,9 +2676,7 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  .Select(video =>
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      // Try to retrieve it from the db. If we don't find it, use the resolved version
 | 
	
		
			
				|  |  | -                    var dbItem = GetItemById(video.Id) as Trailer;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                    if (dbItem != null)
 | 
	
		
			
				|  |  | +                    if (GetItemById(video.Id) is Trailer dbItem)
 | 
	
		
			
				|  |  |                      {
 | 
	
		
			
				|  |  |                          video = dbItem;
 | 
	
		
			
				|  |  |                      }
 | 
	
	
		
			
				|  | @@ -2897,7 +2909,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                  catch (HttpException ex)
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  | -                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
 | 
	
		
			
				|  |  | +                    if (ex.StatusCode.HasValue
 | 
	
		
			
				|  |  | +                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
 | 
	
		
			
				|  |  |                      {
 | 
	
		
			
				|  |  |                          continue;
 | 
	
		
			
				|  |  |                      }
 | 
	
	
		
			
				|  | @@ -2990,23 +3003,6 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |              });
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        private static bool ValidateNetworkPath(string path)
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
 | 
	
		
			
				|  |  | -            //{
 | 
	
		
			
				|  |  | -            //    // We can't validate protocol-based paths, so just allow them
 | 
	
		
			
				|  |  | -            //    if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
 | 
	
		
			
				|  |  | -            //    {
 | 
	
		
			
				|  |  | -            //        return Directory.Exists(path);
 | 
	
		
			
				|  |  | -            //    }
 | 
	
		
			
				|  |  | -            //}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -            // Without native support for unc, we cannot validate this when running under mono
 | 
	
		
			
				|  |  | -            return true;
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        private const string ShortcutFileExtension = ".mblink";
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              AddMediaPathInternal(virtualFolderName, pathInfo, true);
 | 
	
	
		
			
				|  | @@ -3031,11 +3027,6 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  throw new FileNotFoundException("The path does not exist.");
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
 | 
	
		
			
				|  |  | -            {
 | 
	
		
			
				|  |  | -                throw new FileNotFoundException("The network path does not exist.");
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |              var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 | 
	
		
			
				|  |  |              var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -3074,11 +3065,6 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |                  throw new ArgumentNullException(nameof(pathInfo));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
 | 
	
		
			
				|  |  | -            {
 | 
	
		
			
				|  |  | -                throw new FileNotFoundException("The network path does not exist.");
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |              var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 | 
	
		
			
				|  |  |              var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -3210,7 +3196,8 @@ namespace Emby.Server.Implementations.Library
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (!Directory.Exists(virtualFolderPath))
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
 | 
	
		
			
				|  |  | +                throw new FileNotFoundException(
 | 
	
		
			
				|  |  | +                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 |