Procházet zdrojové kódy

Merge with default

ebr11 Eric Reed spam před 12 roky
rodič
revize
f385fe2f20
25 změnil soubory, kde provedl 772 přidání a 258 odebrání
  1. 1 1
      MediaBrowser.Api/ApiService.cs
  2. 1 1
      MediaBrowser.Api/HttpHandlers/GenreHandler.cs
  3. 1 1
      MediaBrowser.Api/HttpHandlers/GenresHandler.cs
  4. 1 1
      MediaBrowser.Api/HttpHandlers/PersonHandler.cs
  5. 1 1
      MediaBrowser.Api/HttpHandlers/StudioHandler.cs
  6. 1 1
      MediaBrowser.Api/HttpHandlers/StudiosHandler.cs
  7. 1 1
      MediaBrowser.Api/HttpHandlers/YearHandler.cs
  8. 1 1
      MediaBrowser.Api/HttpHandlers/YearsHandler.cs
  9. 31 0
      MediaBrowser.Common/Extensions/BaseExtensions.cs
  10. 1 0
      MediaBrowser.Common/MediaBrowser.Common.csproj
  11. 48 0
      MediaBrowser.Controller/Entities/BaseEntity.cs
  12. 14 4
      MediaBrowser.Controller/Entities/BaseItem.cs
  13. 289 19
      MediaBrowser.Controller/Entities/Folder.cs
  14. 6 9
      MediaBrowser.Controller/IO/DirectoryWatchers.cs
  15. 86 0
      MediaBrowser.Controller/IO/FileSystemHelper.cs
  16. 1 1
      MediaBrowser.Controller/IO/Shortcut.cs
  17. 37 45
      MediaBrowser.Controller/Kernel.cs
  18. 34 0
      MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
  19. 97 149
      MediaBrowser.Controller/Library/ItemController.cs
  20. 26 17
      MediaBrowser.Controller/Library/ItemResolveEventArgs.cs
  21. 3 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  22. 2 1
      MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
  23. 68 0
      MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
  24. 1 1
      MediaBrowser.Controller/Resolvers/FolderResolver.cs
  25. 20 4
      MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs

+ 1 - 1
MediaBrowser.Api/ApiService.cs

@@ -288,7 +288,7 @@ namespace MediaBrowser.Api
 
             if (folder != null)
             {
-                IEnumerable<BaseItem> children = folder.GetParentalAllowedChildren(user);
+                IEnumerable<BaseItem> children = folder.GetChildren(user);
 
                 dto.Children = await Task.WhenAll(children.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
             }

+ 1 - 1
MediaBrowser.Api/HttpHandlers/GenreHandler.cs

@@ -40,7 +40,7 @@ namespace MediaBrowser.Api.HttpHandlers
             int count = 0;
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/GenresHandler.cs

@@ -35,7 +35,7 @@ namespace MediaBrowser.Api.HttpHandlers
             var data = new Dictionary<string, int>();
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/PersonHandler.cs

@@ -38,7 +38,7 @@ namespace MediaBrowser.Api.HttpHandlers
             int count = 0;
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/StudioHandler.cs

@@ -40,7 +40,7 @@ namespace MediaBrowser.Api.HttpHandlers
             int count = 0;
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/StudiosHandler.cs

@@ -35,7 +35,7 @@ namespace MediaBrowser.Api.HttpHandlers
             var data = new Dictionary<string, int>();
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/YearHandler.cs

@@ -38,7 +38,7 @@ namespace MediaBrowser.Api.HttpHandlers
             int count = 0;
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 1 - 1
MediaBrowser.Api/HttpHandlers/YearsHandler.cs

@@ -35,7 +35,7 @@ namespace MediaBrowser.Api.HttpHandlers
             var data = new Dictionary<int, int>();
 
             // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
 
             foreach (var item in allItems)
             {

+ 31 - 0
MediaBrowser.Common/Extensions/BaseExtensions.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+
+namespace MediaBrowser.Common.Extensions
+{
+    public static class BaseExtensions
+    {
+        static MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider();
+
+        public static Guid GetMD5(this string str)
+        {
+            lock (md5Provider)
+            {
+                return new Guid(md5Provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
+            }
+        }
+
+        public static bool ContainsStartsWith(this List<string> lst, string value)
+        {
+            foreach (var str in lst)
+            {
+                if (str.StartsWith(value, StringComparison.OrdinalIgnoreCase)) return true;
+            }
+            return false;
+        }
+    }
+}

+ 1 - 0
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -81,6 +81,7 @@
     <Reference Include="WindowsBase" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Extensions\BaseExtensions.cs" />
     <Compile Include="Kernel\BaseApplicationPaths.cs" />
     <Compile Include="Drawing\DrawingUtils.cs" />
     <Compile Include="Logging\TraceLogger.cs" />

+ 48 - 0
MediaBrowser.Controller/Entities/BaseEntity.cs

@@ -1,4 +1,8 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.IO;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -11,6 +15,10 @@ namespace MediaBrowser.Controller.Entities
 
         public Guid Id { get; set; }
 
+        public string Path { get; set; }
+
+        public Folder Parent { get; set; }
+
         public string PrimaryImagePath { get; set; }
 
         public DateTime DateCreated { get; set; }
@@ -21,5 +29,45 @@ namespace MediaBrowser.Controller.Entities
         {
             return Name;
         }
+
+        protected ItemResolveEventArgs _resolveArgs;
+        /// <summary>
+        /// We attach these to the item so that we only ever have to hit the file system once
+        /// (this includes the children of the containing folder)
+        /// Use ResolveArgs.FileSystemChildren to check for the existence of files instead of File.Exists
+        /// </summary>
+        public ItemResolveEventArgs ResolveArgs
+        {
+            get
+            {
+                if (_resolveArgs == null)
+                {
+                    _resolveArgs = new ItemResolveEventArgs()
+                    {
+                        FileInfo = FileData.GetFileData(this.Path),
+                        Parent = this.Parent,
+                        Cancel = false,
+                        Path = this.Path
+                    };
+                    _resolveArgs = FileSystemHelper.FilterChildFileSystemEntries(_resolveArgs, (this.Parent != null && this.Parent.IsRoot));
+                }
+                return _resolveArgs;
+            }
+            set
+            {
+                _resolveArgs = value;
+            }
+        }
+
+        /// <summary>
+        /// Refresh metadata on us by execution our provider chain
+        /// </summary>
+        /// <returns>true if a provider reports we changed</returns>
+        public bool RefreshMetadata()
+        {
+            Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false);
+            return true;
+        }
+
     }
 }

+ 14 - 4
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1,4 +1,6 @@
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.IO;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -14,10 +16,6 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public DateTime? PremiereDate { get; set; }
 
-        public string Path { get; set; }
-
-        public Folder Parent { get; set; }
-
         public string LogoImagePath { get; set; }
 
         public string ArtImagePath { get; set; }
@@ -134,6 +132,18 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        /// <summary>
+        /// Determine if we have changed vs the passed in copy
+        /// </summary>
+        /// <param name="original"></param>
+        /// <returns></returns>
+        public virtual bool IsChanged(BaseItem original)
+        {
+            bool changed = original.DateModified != this.DateModified;
+            changed |= original.DateCreated != this.DateCreated;
+            return changed;
+        }
+
         /// <summary>
         /// Determines if the item is considered new based on user settings
         /// </summary>

+ 289 - 19
MediaBrowser.Controller/Entities/Folder.cs

@@ -1,5 +1,10 @@
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Resolvers;
 using System;
+using System.Threading.Tasks;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -7,6 +12,28 @@ namespace MediaBrowser.Controller.Entities
 {
     public class Folder : BaseItem
     {
+        #region Events
+        /// <summary>
+        /// Fires whenever a validation routine updates our children.  The added and removed children are properties of the args.
+        /// *** Will fire asynchronously. ***
+        /// </summary>
+        public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
+        protected void OnChildrenChanged(ChildrenChangedEventArgs args)
+        {
+            if (ChildrenChanged != null)
+            {
+                Task.Run( () => 
+                    {
+                        ChildrenChanged(this, args);
+                        Kernel.Instance.OnLibraryChanged(args);
+                    });
+            }
+        }
+
+        #endregion
+
+        public IEnumerable<string> PhysicalLocations { get; set; }
+
         public override bool IsFolder
         {
             get
@@ -24,23 +51,266 @@ namespace MediaBrowser.Controller.Entities
                 return Parent != null && Parent.IsRoot;
             }
         }
+        protected object childLock = new object();
+        protected List<BaseItem> children;
+        protected virtual List<BaseItem> ActualChildren
+        {
+            get
+            {
+                if (children == null)
+                {
+                    LoadChildren();
+                }
+                return children;
+            }
+
+            set
+            {
+                children = value;
+            }
+        }
+
+        /// <summary>
+        /// thread-safe access to the actual children of this folder - without regard to user
+        /// </summary>
+        public IEnumerable<BaseItem> Children
+        {
+            get
+            {
+                lock (childLock)
+                    return ActualChildren.ToList();
+            }
+        }
 
-        public IEnumerable<BaseItem> Children { get; set; }
+        /// <summary>
+        /// thread-safe access to all recursive children of this folder - without regard to user
+        /// </summary>
+        public IEnumerable<BaseItem> RecursiveChildren
+        {
+            get
+            {
+                foreach (var item in Children)
+                {
+                    yield return item;
+
+                    var subFolder = item as Folder;
+
+                    if (subFolder != null)
+                    {
+                        foreach (var subitem in subFolder.RecursiveChildren)
+                        {
+                            yield return subitem;
+                        }
+                    }
+                }
+            }
+        }
+                
+
+        /// <summary>
+        /// Loads and validates our children
+        /// </summary>
+        protected virtual void LoadChildren()
+        {
+            //first - load our children from the repo
+            lock (childLock)
+                children = GetCachedChildren();
+
+            //then kick off a validation against the actual file system
+            Task.Run(() => ValidateChildren());
+        }
+
+        protected bool ChildrenValidating = false;
+
+        /// <summary>
+        /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+        /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+        /// </summary>
+        /// <returns></returns>
+        protected async virtual void ValidateChildren()
+        {
+            if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
+            ChildrenValidating = true;
+            bool changed = false; //this will save us a little time at the end if nothing changes
+            var changedArgs = new ChildrenChangedEventArgs(this);
+            //get the current valid children from filesystem (or wherever)
+            var nonCachedChildren = await GetNonCachedChildren();
+            if (nonCachedChildren == null) return; //nothing to validate
+            //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+            Dictionary<Guid, BaseItem> currentChildren;
+            lock (childLock)
+                currentChildren =  ActualChildren.ToDictionary(i => i.Id);
+            
+            //create a list for our validated children
+            var validChildren = new List<BaseItem>();
+            //now traverse the valid children and find any changed or new items
+            foreach (var child in nonCachedChildren)
+            {
+                BaseItem currentChild;
+                currentChildren.TryGetValue(child.Id, out currentChild);
+                if (currentChild == null)
+                {
+                    //brand new item - needs to be added
+                    changed = true;
+                    changedArgs.ItemsAdded.Add(child);
+                    //Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+")"+ child.Name + "(" + child.Path + ")");
+                    //refresh it
+                    child.RefreshMetadata();
+                    //save it in repo...
+
+                    //and add it to our valid children
+                    validChildren.Add(child);
+                    //fire an added event...?
+                    //if it is a folder we need to validate its children as well
+                    Folder folder = child as Folder;
+                    if (folder != null)
+                    {
+                        folder.ValidateChildren();
+                        //probably need to refresh too...
+                    }
+                }
+                else
+                {
+                    //existing item - check if it has changed
+                    if (currentChild.IsChanged(child))
+                    {
+                        changed = true;
+                        //update resolve args and refresh meta
+                        //  Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
+                        //  will identify this item as the same one
+                        currentChild.ResolveArgs = child.ResolveArgs;
+                        currentChild.RefreshMetadata();
+                        //save it in repo...
+                        validChildren.Add(currentChild);
+                    }
+                    else
+                    {
+                        //current child that didn't change - just put it in the valid children
+                        validChildren.Add(currentChild);
+                    }
+                }
+            }
+
+            //that's all the new and changed ones - now see if there are any that are missing
+            changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
+            changed |= changedArgs.ItemsRemoved != null;
+
+            //now, if anything changed - replace our children
+            if (changed)
+            {
+                lock (childLock)
+                    ActualChildren = validChildren;
+                //and save children in repo...
+
+                //and fire event
+                this.OnChildrenChanged(changedArgs);
+            }
+            ChildrenValidating = false;
+
+        }
+
+        /// <summary>
+        /// Get the children of this folder from the actual file system
+        /// </summary>
+        /// <returns></returns>
+        protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
+        {
+            ItemResolveEventArgs args = new ItemResolveEventArgs()
+            {
+                FileInfo = FileData.GetFileData(this.Path),
+                Parent = this.Parent,
+                Cancel = false,
+                Path = this.Path
+            };
+
+            // Gather child folder and files
+            if (args.IsDirectory)
+            {
+                args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
+
+                bool isVirtualFolder = Parent != null && Parent.IsRoot;
+                args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
+            }
+            else
+            {
+                Logger.LogError("Folder has a path that is not a directory: " + this.Path);
+                return null;
+            }
+
+            if (!EntityResolutionHelper.ShouldResolvePathContents(args))
+            {
+                return null;
+            }
+            return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
+                        .Where(i => i != null).OrderBy(f =>
+                        {
+                            return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
+
+                        });
+
+        }
+
+        /// <summary>
+        /// Resolves a path into a BaseItem
+        /// </summary>
+        protected async Task<BaseItem> GetChild(string path,  WIN32_FIND_DATA? fileInfo = null)
+        {
+            ItemResolveEventArgs args = new ItemResolveEventArgs()
+            {
+                FileInfo = fileInfo ?? FileData.GetFileData(path),
+                Parent = this,
+                Cancel = false,
+                Path = path
+            };
+
+            args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+            args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
+
+            return Kernel.Instance.ResolveItem(args);
+
+        }
+
+        /// <summary>
+        /// Finds child BaseItems for a given Folder
+        /// </summary>
+        protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
+        {
+            Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
+
+            for (int i = 0; i < fileSystemChildren.Length; i++)
+            {
+                var child = fileSystemChildren[i];
+
+                tasks[i] = GetChild(child.Path, child);
+            }
+
+            return tasks;
+        }
+
+
+        /// <summary>
+        /// Get our children from the repo - stubbed for now
+        /// </summary>
+        /// <returns></returns>
+        protected virtual List<BaseItem> GetCachedChildren()
+        {
+            return new List<BaseItem>();
+        }
 
         /// <summary>
         /// Gets allowed children of an item
         /// </summary>
-        public IEnumerable<BaseItem> GetParentalAllowedChildren(User user)
+        public IEnumerable<BaseItem> GetChildren(User user)
         {
-            return Children.Where(c => c.IsParentalAllowed(user));
+            return ActualChildren.Where(c => c.IsParentalAllowed(user));
         }
 
         /// <summary>
         /// Gets allowed recursive children of an item
         /// </summary>
-        public IEnumerable<BaseItem> GetParentalAllowedRecursiveChildren(User user)
+        public IEnumerable<BaseItem> GetRecursiveChildren(User user)
         {
-            foreach (var item in GetParentalAllowedChildren(user))
+            foreach (var item in GetChildren(user))
             {
                 yield return item;
 
@@ -48,7 +318,7 @@ namespace MediaBrowser.Controller.Entities
 
                 if (subFolder != null)
                 {
-                    foreach (var subitem in subFolder.GetParentalAllowedRecursiveChildren(user))
+                    foreach (var subitem in subFolder.GetRecursiveChildren(user))
                     {
                         yield return subitem;
                     }
@@ -63,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var counts = new ItemSpecialCounts();
 
-            IEnumerable<BaseItem> recursiveChildren = GetParentalAllowedRecursiveChildren(user);
+            IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
 
             var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
 
@@ -80,7 +350,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
+            return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
         }
 
         /// <summary>
@@ -88,7 +358,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
+            return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
         }
 
         /// <summary>
@@ -96,7 +366,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
+            return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
         }
 
         /// <summary>
@@ -104,7 +374,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public IEnumerable<BaseItem> GetFavoriteItems(User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(c =>
+            return GetRecursiveChildren(user).Where(c =>
             {
                 UserItemData data = c.GetUserData(user, false);
 
@@ -122,7 +392,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(c =>
+            return GetRecursiveChildren(user).Where(c =>
             {
                 if (c.People != null)
                 {
@@ -139,7 +409,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="personType">Specify this to limit results to a specific PersonType</param>
         public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
         {
-            return GetParentalAllowedRecursiveChildren(user).Where(c =>
+            return GetRecursiveChildren(user).Where(c =>
             {
                 if (c.People != null)
                 {
@@ -155,7 +425,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public List<BaseItem> GetRecentlyAddedItems(User user)
         {
-            return GetRecentlyAddedItems(GetParentalAllowedRecursiveChildren(user), user);
+            return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
         }
 
         /// <summary>
@@ -163,7 +433,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
         {
-            return GetRecentlyAddedUnplayedItems(GetParentalAllowedRecursiveChildren(user), user);
+            return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
         }
 
         /// <summary>
@@ -171,7 +441,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public List<BaseItem> GetInProgressItems(User user)
         {
-            return GetInProgressItems(GetParentalAllowedRecursiveChildren(user), user);
+            return GetInProgressItems(GetRecursiveChildren(user), user);
         }
 
         /// <summary>
@@ -288,7 +558,7 @@ namespace MediaBrowser.Controller.Entities
             base.SetPlayedStatus(user, wasPlayed);
 
             // Now sweep through recursively and update status
-            foreach (BaseItem item in GetParentalAllowedChildren(user))
+            foreach (BaseItem item in GetChildren(user))
             {
                 item.SetPlayedStatus(user, wasPlayed);
             }
@@ -306,7 +576,7 @@ namespace MediaBrowser.Controller.Entities
                 return result;
             }
 
-            foreach (BaseItem item in Children)
+            foreach (BaseItem item in ActualChildren)
             {
                 result = item.FindItemById(id);
 
@@ -329,7 +599,7 @@ namespace MediaBrowser.Controller.Entities
                 return this;
             }
 
-            foreach (BaseItem item in Children)
+            foreach (BaseItem item in ActualChildren)
             {
                 var folder = item as Folder;
 

+ 6 - 9
MediaBrowser.Controller/IO/DirectoryWatchers.cs

@@ -1,4 +1,6 @@
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Extensions;
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -26,23 +28,18 @@ namespace MediaBrowser.Controller.IO
 
             foreach (Folder folder in rootFolder.Children.OfType<Folder>())
             {
-                foreach (Folder subFolder in folder.Children.OfType<Folder>())
+                foreach (string path in folder.PhysicalLocations)
                 {
-                    if (Path.IsPathRooted(subFolder.Path))
+                    if (Path.IsPathRooted(path) && !pathsToWatch.ContainsStartsWith(path))
                     {
-                        string parent = Path.GetDirectoryName(subFolder.Path);
-
-                        if (!pathsToWatch.Contains(parent))
-                        {
-                            pathsToWatch.Add(parent);
-                        }
+                        pathsToWatch.Add(path);
                     }
                 }
             }
 
             foreach (string path in pathsToWatch)
             {
-                var watcher = new FileSystemWatcher(path, "*") { };
+                Logger.LogInfo("Watching directory " + path + " for changes.");
 
                 watcher.IncludeSubdirectories = true;
 

+ 86 - 0
MediaBrowser.Controller/IO/FileSystemHelper.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.IO
+{
+    public static class FileSystemHelper
+    {
+        /// <summary>
+        /// Transforms shortcuts into their actual paths and filters out items that should be ignored
+        /// </summary>
+        public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts)
+        {
+            
+            List<WIN32_FIND_DATA> returnChildren = new List<WIN32_FIND_DATA>();
+            List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
+
+            foreach (var file in args.FileSystemChildren)
+            {
+                // If it's a shortcut, resolve it
+                if (Shortcut.IsShortcut(file.Path))
+                {
+                    string newPath = Shortcut.ResolveShortcut(file.Path);
+                    WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
+
+                    // Find out if the shortcut is pointing to a directory or file
+                    if (newPathData.IsDirectory)
+                    {
+                        // add to our physical locations
+                        args.AdditionalLocations.Add(newPath);
+
+                        // If we're flattening then get the shortcut's children
+                        if (flattenShortcuts)
+                        {
+                            returnChildren.Add(file);
+                            ItemResolveEventArgs newArgs = new ItemResolveEventArgs()
+                            {
+                                FileSystemChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray()
+                            };
+
+                            resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newArgs, false).FileSystemChildren);
+                        }
+                        else
+                        {
+                            returnChildren.Add(newPathData);
+                        }
+                    }
+                    else
+                    {
+                        returnChildren.Add(newPathData);
+                    }
+                }
+                else
+                {
+                    //not a shortcut check to see if we should filter it out
+                    if (EntityResolutionHelper.ShouldResolvePath(file))
+                    {
+                        returnChildren.Add(file);
+                    }
+                    else
+                    {
+                        //filtered - see if it is one of our "indicator" folders and mark it now - no reason to search for it again
+                        args.IsBDFolder |= file.cFileName.Equals("bdmv", StringComparison.OrdinalIgnoreCase);
+                        args.IsDVDFolder |= file.cFileName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
+                    }
+                }
+            }
+
+            if (resolvedShortcuts.Count > 0)
+            {
+                resolvedShortcuts.InsertRange(0, returnChildren);
+                args.FileSystemChildren = resolvedShortcuts.ToArray();
+            }
+            else
+            {
+                args.FileSystemChildren = returnChildren.ToArray();
+            }
+            return args;
+        }
+
+    }
+}

+ 1 - 1
MediaBrowser.Controller/IO/Shortcut.cs

@@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.IO
 
         public static bool IsShortcut(string filename)
         {
-            return Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase);
+            return filename != null ? Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase) : false;
         }
     }
 }

+ 37 - 45
MediaBrowser.Controller/Kernel.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Weather;
 using MediaBrowser.Model.Authentication;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Progress;
+using MediaBrowser.Common.Extensions;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.Composition;
@@ -24,6 +25,21 @@ namespace MediaBrowser.Controller
 {
     public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
     {
+        #region Events
+        /// <summary>
+        /// Fires whenever any validation routine adds or removes items.  The added and removed items are properties of the args.
+        /// *** Will fire asynchronously. ***
+        /// </summary>
+        public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
+        public void OnLibraryChanged(ChildrenChangedEventArgs args)
+        {
+            if (LibraryChanged != null)
+            {
+                Task.Run(() => LibraryChanged(this, args));
+            }
+        }
+
+        #endregion
         public static Kernel Instance { get; private set; }
 
         public ItemController ItemController { get; private set; }
@@ -88,8 +104,6 @@ namespace MediaBrowser.Controller
             ItemController = new ItemController();
             DirectoryWatchers = new DirectoryWatchers();
 
-            ItemController.PreBeginResolvePath += ItemController_PreBeginResolvePath;
-            ItemController.BeginResolvePath += ItemController_BeginResolvePath;
 
             base.InitializeInternal(progress);
         }
@@ -139,46 +153,21 @@ namespace MediaBrowser.Controller
             MetadataProviders = MetadataProvidersEnumerable.OrderBy(e => e.Priority).ToArray();
         }
 
-        /// <summary>
-        /// Fires when a path is about to be resolved, but before child folders and files 
-        /// have been collected from the file system.
-        /// This gives us a chance to cancel it if needed, resulting in the path being ignored
-        /// </summary>
-        void ItemController_PreBeginResolvePath(object sender, PreBeginResolveEventArgs e)
+        public BaseItem ResolveItem(ItemResolveEventArgs args)
         {
-            // Ignore hidden files and folders
-            if (e.IsHidden || e.IsSystemFile)
-            {
-                e.Cancel = true;
-            }
-
-            // Ignore any folders named "trailers"
-            else if (Path.GetFileName(e.Path).Equals("trailers", StringComparison.OrdinalIgnoreCase))
+            // Try first priority resolvers
+            for (int i = 0; i < EntityResolvers.Length; i++)
             {
-                e.Cancel = true;
-            }
+                var item = EntityResolvers[i].ResolvePath(args);
 
-            // Don't try and resolve files within the season metadata folder
-            else if (Path.GetFileName(e.Path).Equals("metadata", StringComparison.OrdinalIgnoreCase) && e.IsDirectory)
-            {
-                if (e.Parent is Season || e.Parent is Series)
+                if (item != null)
                 {
-                    e.Cancel = true;
+                    item.ResolveArgs = args;
+                    return item;
                 }
             }
-        }
 
-        /// <summary>
-        /// Fires when a path is about to be resolved, but after child folders and files 
-        /// This gives us a chance to cancel it if needed, resulting in the path being ignored
-        /// </summary>
-        void ItemController_BeginResolvePath(object sender, ItemResolveEventArgs e)
-        {
-            if (e.ContainsFile(".ignore"))
-            {
-                // Ignore any folders containing a file called .ignore
-                e.Cancel = true;
-            }
+            return null;
         }
 
         private void ReloadUsers()
@@ -203,12 +192,14 @@ namespace MediaBrowser.Controller
             DirectoryWatchers.Start();
         }
 
-        public static Guid GetMD5(string str)
+        void RootFolder_ChildrenChanged(object sender, ChildrenChangedEventArgs e)
         {
-            using (var provider = new MD5CryptoServiceProvider())
-            {
-                return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
-            }
+            Logger.LogDebugInfo("Root Folder Children Changed.  Added: " + e.ItemsAdded.Count + " Removed: " + e.ItemsRemoved.Count());
+            //re-start the directory watchers
+            DirectoryWatchers.Stop();
+            DirectoryWatchers.Start();
+            var allChildren = RootFolder.RecursiveChildren;
+            Logger.LogInfo(string.Format("Loading complete.  Movies: {0} Episodes: {1}", allChildren.OfType<Entities.Movies.Movie>().Count(), allChildren.OfType<Entities.TV.Episode>().Count()));
         }
 
         /// <summary>
@@ -247,7 +238,8 @@ namespace MediaBrowser.Controller
             }
             else
             {
-                result.Success = GetMD5((password ?? string.Empty)).ToString().Equals(user.Password);
+                password = password ?? string.Empty;
+                result.Success = password.GetMD5().ToString().Equals(user.Password);
             }
 
             // Update LastActivityDate and LastLoginDate, then save
@@ -286,7 +278,7 @@ namespace MediaBrowser.Controller
 
                 children.Insert(index, newItem);
 
-                item.Parent.Children = children.ToArray();
+                //item.Parent.ActualChildren = children.ToArray();
             }
         }
 
@@ -323,7 +315,7 @@ namespace MediaBrowser.Controller
             user.Id = Guid.NewGuid();
             user.LastLoginDate = DateTime.UtcNow.AddDays(-1);
             user.LastActivityDate = DateTime.UtcNow.AddHours(-3);
-            user.Password = GetMD5("1234").ToString();
+            user.Password = ("1234").GetMD5().ToString();
             list.Add(user);
 
             user = new User { };
@@ -357,7 +349,7 @@ namespace MediaBrowser.Controller
         /// <summary>
         /// Runs all metadata providers for an entity
         /// </summary>
-        internal async Task ExecuteMetadataProviders(BaseEntity item, ItemResolveEventArgs args, bool allowInternetProviders = true)
+        internal async Task ExecuteMetadataProviders(BaseEntity item, bool allowInternetProviders = true)
         {
             // Run them sequentially in order of priority
             for (int i = 0; i < MetadataProviders.Length; i++)
@@ -378,7 +370,7 @@ namespace MediaBrowser.Controller
 
                 try
                 {
-                    await provider.FetchAsync(item, args).ConfigureAwait(false);
+                    await provider.FetchAsync(item, item.ResolveArgs).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {

+ 34 - 0
MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+    public class ChildrenChangedEventArgs : EventArgs
+    {
+        public Folder Folder { get; set; }
+        public List<BaseItem> ItemsAdded { get; set; }
+        public IEnumerable<BaseItem> ItemsRemoved { get; set; }
+
+        public ChildrenChangedEventArgs()
+        {
+            //initialize the list
+            ItemsAdded = new List<BaseItem>();
+        }
+
+        /// <summary>
+        /// Create the args and set the folder property
+        /// </summary>
+        /// <param name="folder"></param>
+        public ChildrenChangedEventArgs(Folder folder)
+        {
+            //init the folder property
+            this.Folder = folder;
+            //init the list
+            ItemsAdded = new List<BaseItem>();
+        }
+    }
+}

+ 97 - 149
MediaBrowser.Controller/Library/ItemController.cs

@@ -1,5 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Common.Extensions;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -11,57 +13,21 @@ namespace MediaBrowser.Controller.Library
 {
     public class ItemController
     {
-        #region PreBeginResolvePath Event
-        /// <summary>
-        /// Fires when a path is about to be resolved, but before child folders and files 
-        /// have been collected from the file system.
-        /// This gives listeners a chance to cancel the operation and cause the path to be ignored.
-        /// </summary>
-        public event EventHandler<PreBeginResolveEventArgs> PreBeginResolvePath;
-        private bool OnPreBeginResolvePath(PreBeginResolveEventArgs args)
-        {
-            if (PreBeginResolvePath != null)
-            {
-                PreBeginResolvePath(this, args);
-            }
-
-            return !args.Cancel;
-        }
-        #endregion
-
-        #region BeginResolvePath Event
-        /// <summary>
-        /// Fires when a path is about to be resolved, but after child folders and files 
-        /// have been collected from the file system.
-        /// This gives listeners a chance to cancel the operation and cause the path to be ignored.
-        /// </summary>
-        public event EventHandler<ItemResolveEventArgs> BeginResolvePath;
-        private bool OnBeginResolvePath(ItemResolveEventArgs args)
-        {
-            if (BeginResolvePath != null)
-            {
-                BeginResolvePath(this, args);
-            }
-
-            return !args.Cancel;
-        }
-        #endregion
-
-        private BaseItem ResolveItem(ItemResolveEventArgs args)
-        {
-            // Try first priority resolvers
-            for (int i = 0; i < Kernel.Instance.EntityResolvers.Length; i++)
-            {
-                var item = Kernel.Instance.EntityResolvers[i].ResolvePath(args);
-
-                if (item != null)
-                {
-                    return item;
-                }
-            }
-
-            return null;
-        }
+        //private BaseItem ResolveItem(ItemResolveEventArgs args)
+        //{
+        //    // Try first priority resolvers
+        //    for (int i = 0; i < Kernel.Instance.EntityResolvers.Length; i++)
+        //    {
+        //        var item = Kernel.Instance.EntityResolvers[i].ResolvePath(args);
+
+        //        if (item != null)
+        //        {
+        //            return item;
+        //        }
+        //    }
+
+        //    return null;
+        //}
 
         /// <summary>
         /// Resolves a path into a BaseItem
@@ -76,122 +42,104 @@ namespace MediaBrowser.Controller.Library
                 Path = path
             };
 
-            if (!OnPreBeginResolvePath(args))
-            {
-                return null;
-            }
-
-            WIN32_FIND_DATA[] fileSystemChildren;
-
             // Gather child folder and files
             if (args.IsDirectory)
             {
-                fileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+                args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
 
                 bool isVirtualFolder = parent != null && parent.IsRoot;
-                fileSystemChildren = FilterChildFileSystemEntries(fileSystemChildren, isVirtualFolder);
+                args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
             }
             else
             {
-                fileSystemChildren = new WIN32_FIND_DATA[] { };
+                args.FileSystemChildren = new WIN32_FIND_DATA[] { };
             }
 
-            args.FileSystemChildren = fileSystemChildren;
 
             // Fire BeginResolvePath to see if anyone wants to cancel this operation
-            if (!OnBeginResolvePath(args))
+            if (!EntityResolutionHelper.ShouldResolvePathContents(args))
             {
                 return null;
             }
 
-            BaseItem item = ResolveItem(args);
-
-            if (item != null)
-            {
-                await Kernel.Instance.ExecuteMetadataProviders(item, args, allowInternetProviders: allowInternetProviders).ConfigureAwait(false);
-
-                if (item.IsFolder)
-                {
-                    // If it's a folder look for child entities
-                    (item as Folder).Children = (await Task.WhenAll(GetChildren(item as Folder, fileSystemChildren, allowInternetProviders)).ConfigureAwait(false))
-                        .Where(i => i != null).OrderBy(f => (string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName));
-                }
-            }
+            BaseItem item = Kernel.Instance.ResolveItem(args);
 
             return item;
         }
 
-        /// <summary>
-        /// Finds child BaseItems for a given Folder
-        /// </summary>
-        private Task<BaseItem>[] GetChildren(Folder folder, WIN32_FIND_DATA[] fileSystemChildren, bool allowInternetProviders)
-        {
-            var tasks = new Task<BaseItem>[fileSystemChildren.Length];
-
-            for (int i = 0; i < fileSystemChildren.Length; i++)
-            {
-                var child = fileSystemChildren[i];
-
-                tasks[i] = GetItem(child.Path, folder, child, allowInternetProviders: allowInternetProviders);
-            }
-
-            return tasks;
-        }
-
-        /// <summary>
-        /// Transforms shortcuts into their actual paths
-        /// </summary>
-        private WIN32_FIND_DATA[] FilterChildFileSystemEntries(WIN32_FIND_DATA[] fileSystemChildren, bool flattenShortcuts)
-        {
-            var returnArray = new WIN32_FIND_DATA[fileSystemChildren.Length];
-            var resolvedShortcuts = new List<WIN32_FIND_DATA>();
-
-            for (int i = 0; i < fileSystemChildren.Length; i++)
-            {
-                WIN32_FIND_DATA file = fileSystemChildren[i];
-
-                // If it's a shortcut, resolve it
-                if (Shortcut.IsShortcut(file.Path))
-                {
-                    string newPath = Shortcut.ResolveShortcut(file.Path);
-                    WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
-
-                    // Find out if the shortcut is pointing to a directory or file
-                    if (newPathData.IsDirectory)
-                    {
-                        // If we're flattening then get the shortcut's children
-
-                        if (flattenShortcuts)
-                        {
-                            returnArray[i] = file;
-                            WIN32_FIND_DATA[] newChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray();
-
-                            resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newChildren, false));
-                        }
-                        else
-                        {
-                            returnArray[i] = newPathData;
-                        }
-                    }
-                    else
-                    {
-                        returnArray[i] = newPathData;
-                    }
-                }
-                else
-                {
-                    returnArray[i] = file;
-                }
-            }
-
-            if (resolvedShortcuts.Count > 0)
-            {
-                resolvedShortcuts.InsertRange(0, returnArray);
-                return resolvedShortcuts.ToArray();
-            }
-
-            return returnArray;
-        }
+        ///// <summary>
+        ///// Finds child BaseItems for a given Folder
+        ///// </summary>
+        //private Task<BaseItem>[] GetChildren(Folder folder, WIN32_FIND_DATA[] fileSystemChildren, bool allowInternetProviders)
+        //{
+        //    Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
+
+        //    for (int i = 0; i < fileSystemChildren.Length; i++)
+        //    {
+        //        var child = fileSystemChildren[i];
+
+        //        tasks[i] = GetItem(child.Path, folder, child, allowInternetProviders: allowInternetProviders);
+        //    }
+
+        //    return tasks;
+        //}
+
+        ///// <summary>
+        ///// Transforms shortcuts into their actual paths
+        ///// </summary>
+        //private WIN32_FIND_DATA[] FilterChildFileSystemEntries(WIN32_FIND_DATA[] fileSystemChildren, bool flattenShortcuts)
+        //{
+        //    WIN32_FIND_DATA[] returnArray = new WIN32_FIND_DATA[fileSystemChildren.Length];
+        //    List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
+
+        //    for (int i = 0; i < fileSystemChildren.Length; i++)
+        //    {
+        //        WIN32_FIND_DATA file = fileSystemChildren[i];
+
+        //        // If it's a shortcut, resolve it
+        //        if (Shortcut.IsShortcut(file.Path))
+        //        {
+        //            string newPath = Shortcut.ResolveShortcut(file.Path);
+        //            WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
+
+        //            // Find out if the shortcut is pointing to a directory or file
+        //            if (newPathData.IsDirectory)
+        //            {
+        //                // If we're flattening then get the shortcut's children
+
+        //                if (flattenShortcuts)
+        //                {
+        //                    returnArray[i] = file;
+        //                    WIN32_FIND_DATA[] newChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray();
+
+        //                    resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newChildren, false));
+        //                }
+        //                else
+        //                {
+        //                    returnArray[i] = newPathData;
+        //                }
+        //            }
+        //            else
+        //            {
+        //                returnArray[i] = newPathData;
+        //            }
+        //        }
+        //        else
+        //        {
+        //            returnArray[i] = file;
+        //        }
+        //    }
+
+        //    if (resolvedShortcuts.Count > 0)
+        //    {
+        //        resolvedShortcuts.InsertRange(0, returnArray);
+        //        return resolvedShortcuts.ToArray();
+        //    }
+        //    else
+        //    {
+        //        return returnArray;
+        //    }
+        //}
 
         /// <summary>
         /// Gets a Person
@@ -255,7 +203,7 @@ namespace MediaBrowser.Controller.Library
             var item = new T { };
 
             item.Name = name;
-            item.Id = Kernel.GetMD5(path);
+            item.Id = path.GetMD5();
 
             if (!Directory.Exists(path))
             {
@@ -269,7 +217,7 @@ namespace MediaBrowser.Controller.Library
             args.FileInfo = FileData.GetFileData(path);
             args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
 
-            await Kernel.Instance.ExecuteMetadataProviders(item, args).ConfigureAwait(false);
+            await Kernel.Instance.ExecuteMetadataProviders(item).ConfigureAwait(false);
 
             return item;
         }

+ 26 - 17
MediaBrowser.Controller/Library/ItemResolveEventArgs.cs

@@ -1,5 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.IO;
+using System.Collections.Generic;
+using System.Linq;
 using System;
 using System.IO;
 
@@ -12,32 +14,39 @@ namespace MediaBrowser.Controller.Library
     {
         public WIN32_FIND_DATA[] FileSystemChildren { get; set; }
 
-        public WIN32_FIND_DATA? GetFileSystemEntry(string path)
+        protected List<string> _additionalLocations = new List<string>();
+        public List<string> AdditionalLocations
         {
-            for (int i = 0; i < FileSystemChildren.Length; i++)
+            get
             {
-                WIN32_FIND_DATA entry = FileSystemChildren[i];
-
-                if (entry.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
-                {
-                    return entry;
-                }
+                return _additionalLocations;
+            }
+            set
+            {
+                _additionalLocations = value;
             }
-           
-            return null;
         }
 
-        public bool ContainsFile(string name)
+        public IEnumerable<string> PhysicalLocations
         {
-            for (int i = 0; i < FileSystemChildren.Length; i++)
+            get
             {
-                if (FileSystemChildren[i].cFileName.Equals(name, StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
+                return (new List<string>() {this.Path}).Concat(AdditionalLocations);
             }
+        }
 
-            return false;
+        public bool IsBDFolder { get; set; }
+        public bool IsDVDFolder { get; set; }
+
+        public WIN32_FIND_DATA? GetFileSystemEntry(string path)
+        {
+            WIN32_FIND_DATA entry = FileSystemChildren.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+            return entry.cFileName != null ? (WIN32_FIND_DATA?)entry : null;
+        }
+
+        public bool ContainsFile(string name)
+        {
+            return FileSystemChildren.FirstOrDefault(f => f.cFileName.Equals(name, StringComparison.OrdinalIgnoreCase)).cFileName != null;
         }
 
         public bool ContainsFolder(string name)

+ 3 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -74,6 +74,8 @@
     <Compile Include="Entities\UserItemData.cs" />
     <Compile Include="Entities\Video.cs" />
     <Compile Include="Entities\Year.cs" />
+    <Compile Include="IO\FileSystemHelper.cs" />
+    <Compile Include="Library\ChildrenChangedEventArgs.cs" />
     <Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
     <Compile Include="Providers\Movies\MovieSpecialFeaturesProvider.cs" />
     <Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
@@ -81,6 +83,7 @@
     <Compile Include="Providers\TV\EpisodeXmlParser.cs" />
     <Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
     <Compile Include="Providers\TV\SeriesXmlParser.cs" />
+    <Compile Include="Resolvers\EntityResolutionHelper.cs" />
     <Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
     <Compile Include="Resolvers\Movies\MovieResolver.cs" />
     <Compile Include="Resolvers\TV\EpisodeResolver.cs" />

+ 2 - 1
MediaBrowser.Controller/Resolvers/BaseItemResolver.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Extensions;
 using System;
 using System.IO;
 
@@ -39,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
                 item.Parent = args.Parent;
             }
 
-            item.Id = Kernel.GetMD5(item.Path);
+            item.Id = (item.GetType().FullName + item.Path).GetMD5();
         }
 
         public BaseItem ResolvePath(ItemResolveEventArgs args)

+ 68 - 0
MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Entities.TV;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+    public static class EntityResolutionHelper
+    {
+        /// <summary>
+        /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+        /// </summary>
+        public static List<string> IgnoreFolders = new List<string>()
+        {
+            "trailers",
+            "metadata",
+            "bdmv",
+            "certificate",
+            "backup",
+            "video_ts",
+            "audio_ts",
+            "ps3_update",
+            "ps3_vprm"
+        };
+        /// <summary>
+        /// Determines whether a path should be resolved or ignored entirely - called before we even look at the contents
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns>false if the path should be ignored</returns>
+        public static bool ShouldResolvePath(WIN32_FIND_DATA path)
+        {
+            bool resolve = true;
+            // Ignore hidden files and folders
+            if (path.IsHidden || path.IsSystemFile)
+            {
+                resolve = false;
+            }
+
+            // Ignore any folders in our list
+            else if (path.IsDirectory && IgnoreFolders.Contains(Path.GetFileName(path.Path), StringComparer.OrdinalIgnoreCase))
+            {
+                resolve =  false;
+            }
+
+            return resolve;
+        }
+
+        /// <summary>
+        /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+        /// </summary>
+        public static bool ShouldResolvePathContents(ItemResolveEventArgs args)
+        {
+            bool resolve = true;
+            if (args.ContainsFile(".ignore"))
+            {
+                // Ignore any folders containing a file called .ignore
+                resolve = false;
+            }
+
+            return resolve;
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Controller/Resolvers/FolderResolver.cs

@@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.Resolvers
         {
             if (args.IsDirectory)
             {
-                return new Folder();
+                return new Folder() { PhysicalLocations = args.PhysicalLocations };
             }
 
             return null;

+ 20 - 4
MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
 using System.ComponentModel.Composition;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Resolvers.Movies
 {
@@ -61,12 +62,27 @@ namespace MediaBrowser.Controller.Resolvers.Movies
 
         private Movie GetMovie(ItemResolveEventArgs args)
         {
-            // Loop through each child file/folder and see if we find a video
-            for (var i = 0; i < args.FileSystemChildren.Length; i++)
+            //first see if the discovery process has already determined we are a DVD or BD
+            if (args.IsDVDFolder)
+            {
+                return new Movie()
+                {
+                    Path = args.Path,
+                    VideoType = VideoType.DVD
+                };
+            }
+            else if (args.IsBDFolder)
             {
-                var child = args.FileSystemChildren[i];
+                return new Movie()
+                {
+                    Path = args.Path,
+                    VideoType = VideoType.BluRay
+                };
+            }
 
-                var childArgs = new ItemResolveEventArgs
+            // Loop through each child file/folder and see if we find a video
+            foreach (var child in args.FileSystemChildren)
+            {
                 {
                     FileInfo = child,
                     FileSystemChildren = new WIN32_FIND_DATA[] { },