Преглед изворни кода

Added initial implementation of the metadata provider network, along with the first few providers

LukePulverenti Luke Pulverenti luke pulverenti пре 13 година
родитељ
комит
d794eecec4

+ 12 - 8
MediaBrowser.Common/Kernel/BaseKernel.cs

@@ -12,6 +12,7 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Serialization;
 using MediaBrowser.Common.Serialization;
 using MediaBrowser.Model.Progress;
 using MediaBrowser.Model.Progress;
+using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Common.Kernel
 namespace MediaBrowser.Common.Kernel
 {
 {
@@ -51,18 +52,21 @@ namespace MediaBrowser.Common.Kernel
             ApplicationPaths = new TApplicationPathsType();
             ApplicationPaths = new TApplicationPathsType();
         }
         }
 
 
-        public virtual void Init(IProgress<TaskProgress> progress)
+        public virtual Task Init(IProgress<TaskProgress> progress)
         {
         {
-            ReloadLogger();
+            return Task.Run(() =>
+            {
+                ReloadLogger();
 
 
-            progress.Report(new TaskProgress() { Description = "Loading configuration", PercentComplete = 0 });
-            ReloadConfiguration();
+                progress.Report(new TaskProgress() { Description = "Loading configuration", PercentComplete = 0 });
+                ReloadConfiguration();
 
 
-            progress.Report(new TaskProgress() { Description = "Starting Http server", PercentComplete = 5 });
-            ReloadHttpServer();
+                progress.Report(new TaskProgress() { Description = "Starting Http server", PercentComplete = 5 });
+                ReloadHttpServer();
 
 
-            progress.Report(new TaskProgress() { Description = "Loading Plugins", PercentComplete = 10 });
-            ReloadComposableParts();
+                progress.Report(new TaskProgress() { Description = "Loading Plugins", PercentComplete = 10 });
+                ReloadComposableParts();
+            });
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 46 - 2
MediaBrowser.Controller/Configuration/ServerApplicationPaths.cs

@@ -151,6 +151,50 @@ namespace MediaBrowser.Controller.Configuration
             }
             }
         }
         }
 
 
+        private string _CacheDirectory = null;
+        /// <summary>
+        /// Gets the folder path to the cache directory
+        /// </summary>
+        public string CacheDirectory
+        {
+            get
+            {
+                if (_CacheDirectory == null)
+                {
+                    _CacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "cache");
+
+                    if (!Directory.Exists(_CacheDirectory))
+                    {
+                        Directory.CreateDirectory(_CacheDirectory);
+                    }
+                }
+
+                return _CacheDirectory;
+            }
+        }
+
+        private string _FFProbeAudioCacheDirectory = null;
+        /// <summary>
+        /// Gets the folder path to the ffprobe audio cache directory
+        /// </summary>
+        public string FFProbeAudioCacheDirectory
+        {
+            get
+            {
+                if (_FFProbeAudioCacheDirectory == null)
+                {
+                    _FFProbeAudioCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-audio");
+
+                    if (!Directory.Exists(_FFProbeAudioCacheDirectory))
+                    {
+                        Directory.CreateDirectory(_FFProbeAudioCacheDirectory);
+                    }
+                }
+
+                return _FFProbeAudioCacheDirectory;
+            }
+        }
+        
         private string _FFMpegDirectory = null;
         private string _FFMpegDirectory = null;
         /// <summary>
         /// <summary>
         /// Gets the folder path to ffmpeg
         /// Gets the folder path to ffmpeg
@@ -221,7 +265,7 @@ namespace MediaBrowser.Controller.Configuration
 
 
                     _FFProbePath = Path.Combine(FFMpegDirectory, filename);
                     _FFProbePath = Path.Combine(FFMpegDirectory, filename);
 
 
-                    // Always re-extract the first time to handle new versions
+                    /*// Always re-extract the first time to handle new versions
                     if (File.Exists(_FFProbePath))
                     if (File.Exists(_FFProbePath))
                     {
                     {
                         File.Delete(_FFProbePath);
                         File.Delete(_FFProbePath);
@@ -234,7 +278,7 @@ namespace MediaBrowser.Controller.Configuration
                         {
                         {
                             stream.CopyTo(fileStream);
                             stream.CopyTo(fileStream);
                         }
                         }
-                    }
+                    }*/
                 }
                 }
 
 
                 return _FFProbePath;
                 return _FFProbePath;

+ 42 - 8
MediaBrowser.Controller/FFMpeg/FFProbe.cs

@@ -1,13 +1,41 @@
 using System;
 using System;
 using System.Diagnostics;
 using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
 using MediaBrowser.Common.Logging;
 using MediaBrowser.Common.Logging;
 using MediaBrowser.Common.Serialization;
 using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.FFMpeg
 namespace MediaBrowser.Controller.FFMpeg
 {
 {
+    /// <summary>
+    /// Runs FFProbe against a media file and returns metadata.
+    /// </summary>
     public static class FFProbe
     public static class FFProbe
     {
     {
-        public static FFProbeResult Run(string path)
+        public async static Task<FFProbeResult> Run(Audio item, string outputCachePath)
+        {
+            // Use try catch to avoid having to use File.Exists
+            try
+            {
+                using (FileStream stream = File.OpenRead(outputCachePath))
+                {
+                    return JsonSerializer.DeserializeFromStream<FFProbeResult>(stream);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+            }
+
+            await Run(item.Path, outputCachePath);
+
+            using (FileStream stream = File.OpenRead(outputCachePath))
+            {
+                return JsonSerializer.DeserializeFromStream<FFProbeResult>(stream);
+            }
+        }
+
+        private async static Task Run(string input, string output)
         {
         {
             ProcessStartInfo startInfo = new ProcessStartInfo();
             ProcessStartInfo startInfo = new ProcessStartInfo();
 
 
@@ -21,13 +49,15 @@ namespace MediaBrowser.Controller.FFMpeg
 
 
             startInfo.FileName = Kernel.Instance.ApplicationPaths.FFProbePath;
             startInfo.FileName = Kernel.Instance.ApplicationPaths.FFProbePath;
             startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
             startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
-            startInfo.Arguments = string.Format("\"{0}\" -v quiet -print_format json -show_streams -show_format", path);
+            startInfo.Arguments = string.Format("\"{0}\" -v quiet -print_format json -show_streams -show_format", input);
 
 
-            Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
+            //Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
 
 
             Process process = new Process();
             Process process = new Process();
             process.StartInfo = startInfo;
             process.StartInfo = startInfo;
 
 
+            FileStream stream = new FileStream(output, FileMode.Create);
+
             try
             try
             {
             {
                 process.Start();
                 process.Start();
@@ -36,18 +66,23 @@ namespace MediaBrowser.Controller.FFMpeg
                 // If we ever decide to disable the ffmpeg log then you must uncomment the below line.
                 // If we ever decide to disable the ffmpeg log then you must uncomment the below line.
                 process.BeginErrorReadLine();
                 process.BeginErrorReadLine();
 
 
-                FFProbeResult result = JsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
+                await process.StandardOutput.BaseStream.CopyToAsync(stream);
 
 
                 process.WaitForExit();
                 process.WaitForExit();
 
 
-                Logger.LogInfo("FFMpeg exited with code " + process.ExitCode);
+                stream.Dispose();
 
 
-                return result;
+                if (process.ExitCode != 0)
+                {
+                    Logger.LogInfo("FFProbe exited with code {0} for {1}", process.ExitCode, input);
+                }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 Logger.LogException(ex);
                 Logger.LogException(ex);
 
 
+                stream.Dispose();
+
                 // Hate having to do this
                 // Hate having to do this
                 try
                 try
                 {
                 {
@@ -56,8 +91,7 @@ namespace MediaBrowser.Controller.FFMpeg
                 catch
                 catch
                 {
                 {
                 }
                 }
-
-                return null;
+                File.Delete(output);
             }
             }
             finally
             finally
             {
             {

+ 21 - 33
MediaBrowser.Controller/FFMpeg/FFProbeResult.cs

@@ -13,6 +13,11 @@ namespace MediaBrowser.Controller.FFMpeg
         public MediaFormat format { get; set; }
         public MediaFormat format { get; set; }
     }
     }
 
 
+    /// <summary>
+    /// Represents a stream within the output
+    /// A number of properties are commented out to improve deserialization performance
+    /// Enable them as needed.
+    /// </summary>
     public class MediaStream
     public class MediaStream
     {
     {
         public int index { get; set; }
         public int index { get; set; }
@@ -20,28 +25,28 @@ namespace MediaBrowser.Controller.FFMpeg
         public string codec_name { get; set; }
         public string codec_name { get; set; }
         public string codec_long_name { get; set; }
         public string codec_long_name { get; set; }
         public string codec_type { get; set; }
         public string codec_type { get; set; }
-        public string codec_time_base { get; set; }
-        public string codec_tag { get; set; }
-        public string codec_tag_string { get; set; }
-        public string sample_fmt { get; set; }
+        //public string codec_time_base { get; set; }
+        //public string codec_tag { get; set; }
+        //public string codec_tag_string { get; set; }
+        //public string sample_fmt { get; set; }
         public string sample_rate { get; set; }
         public string sample_rate { get; set; }
         public int channels { get; set; }
         public int channels { get; set; }
-        public int bits_per_sample { get; set; }
-        public string r_frame_rate { get; set; }
-        public string avg_frame_rate { get; set; }
-        public string time_base { get; set; }
-        public string start_time { get; set; }
+        //public int bits_per_sample { get; set; }
+        //public string r_frame_rate { get; set; }
+        //public string avg_frame_rate { get; set; }
+        //public string time_base { get; set; }
+        //public string start_time { get; set; }
         public string duration { get; set; }
         public string duration { get; set; }
         public string bit_rate { get; set; }
         public string bit_rate { get; set; }
 
 
         public int width { get; set; }
         public int width { get; set; }
         public int height { get; set; }
         public int height { get; set; }
-        public int has_b_frames { get; set; }
-        public string sample_aspect_ratio { get; set; }
-        public string display_aspect_ratio { get; set; }
-        public string pix_fmt { get; set; }
-        public int level { get; set; }
-        public MediaTags tags { get; set; }
+        //public int has_b_frames { get; set; }
+        //public string sample_aspect_ratio { get; set; }
+        //public string display_aspect_ratio { get; set; }
+        //public string pix_fmt { get; set; }
+        //public int level { get; set; }
+        public Dictionary<string,string> tags { get; set; }
     }
     }
 
 
     public class MediaFormat
     public class MediaFormat
@@ -54,23 +59,6 @@ namespace MediaBrowser.Controller.FFMpeg
         public string duration { get; set; }
         public string duration { get; set; }
         public string size { get; set; }
         public string size { get; set; }
         public string bit_rate { get; set; }
         public string bit_rate { get; set; }
-        public MediaTags tags { get; set; }
-    }
-
-    public class MediaTags
-    {
-        public string title { get; set; }
-        public string comment { get; set; }
-        public string artist { get; set; }
-        public string album { get; set; }
-        public string album_artist { get; set; }
-        public string composer { get; set; }
-        public string copyright { get; set; }
-        public string publisher { get; set; }
-        public string track { get; set; }
-        public string disc { get; set; }
-        public string genre { get; set; }
-        public string date { get; set; }
-        public string language { get; set; }
+        public Dictionary<string, string> tags { get; set; }
     }
     }
 }
 }

+ 2 - 2
MediaBrowser.Controller/IO/DirectoryWatchers.cs

@@ -88,7 +88,7 @@ namespace MediaBrowser.Controller.IO
 
 
         private void ProcessPathChanges(IEnumerable<string> paths)
         private void ProcessPathChanges(IEnumerable<string> paths)
         {
         {
-            List<BaseItem> itemsToRefresh = new List<BaseItem>();
+            /*List<BaseItem> itemsToRefresh = new List<BaseItem>();
 
 
             foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p)))
             foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p)))
             {
             {
@@ -113,7 +113,7 @@ namespace MediaBrowser.Controller.IO
                 {
                 {
                     Kernel.Instance.ReloadItem(itemsToRefresh[i]);
                     Kernel.Instance.ReloadItem(itemsToRefresh[i]);
                 });
                 });
-            }
+            }*/
         }
         }
 
 
         private BaseItem GetAffectedBaseItem(string path)
         private BaseItem GetAffectedBaseItem(string path)

+ 54 - 18
MediaBrowser.Controller/Kernel.cs

@@ -5,11 +5,13 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
 using System.Text;
 using System.Text;
+using System.Threading.Tasks;
 using MediaBrowser.Common.Kernel;
 using MediaBrowser.Common.Kernel;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Progress;
 using MediaBrowser.Model.Progress;
@@ -35,6 +37,12 @@ namespace MediaBrowser.Controller
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Gets the list of currently registered metadata prvoiders
+        /// </summary>
+        [ImportMany(typeof(BaseMetadataProvider))]
+        public IEnumerable<BaseMetadataProvider> MetadataProviders { get; private set; }
+
         /// <summary>
         /// <summary>
         /// Gets the list of currently registered entity resolvers
         /// Gets the list of currently registered entity resolvers
         /// </summary>
         /// </summary>
@@ -56,35 +64,63 @@ namespace MediaBrowser.Controller
             ItemController.BeginResolvePath += ItemController_BeginResolvePath;
             ItemController.BeginResolvePath += ItemController_BeginResolvePath;
         }
         }
 
 
-        public override void Init(IProgress<TaskProgress> progress)
+        public async override Task Init(IProgress<TaskProgress> progress)
         {
         {
-            base.Init(progress);
+            await base.Init(progress);
 
 
             progress.Report(new TaskProgress() { Description = "Loading Users", PercentComplete = 15 });
             progress.Report(new TaskProgress() { Description = "Loading Users", PercentComplete = 15 });
             ReloadUsers();
             ReloadUsers();
 
 
             progress.Report(new TaskProgress() { Description = "Loading Media Library", PercentComplete = 20 });
             progress.Report(new TaskProgress() { Description = "Loading Media Library", PercentComplete = 20 });
-            ReloadRoot();
+            await ReloadRoot();
 
 
             progress.Report(new TaskProgress() { Description = "Loading Complete", PercentComplete = 100 });
             progress.Report(new TaskProgress() { Description = "Loading Complete", PercentComplete = 100 });
         }
         }
 
 
         protected override void OnComposablePartsLoaded()
         protected override void OnComposablePartsLoaded()
         {
         {
-            List<IBaseItemResolver> resolvers = EntityResolvers.ToList();
-
-            // Add the internal resolvers
-            resolvers.Add(new VideoResolver());
-            resolvers.Add(new AudioResolver());
-            resolvers.Add(new VirtualFolderResolver());
-            resolvers.Add(new FolderResolver());
-
-            EntityResolvers = resolvers;
+            AddCoreResolvers();
+            AddCoreProviders();
 
 
             // The base class will start up all the plugins
             // The base class will start up all the plugins
             base.OnComposablePartsLoaded();
             base.OnComposablePartsLoaded();
         }
         }
 
 
+        private void AddCoreResolvers()
+        {
+            List<IBaseItemResolver> list = EntityResolvers.ToList();
+
+            // Add the core resolvers
+            list.AddRange(new IBaseItemResolver[]{
+                new AudioResolver(),
+                new VideoResolver(),
+                new VirtualFolderResolver(),
+                new FolderResolver()
+            });
+
+            EntityResolvers = list;
+        }
+
+        private void AddCoreProviders()
+        {
+            List<BaseMetadataProvider> list = MetadataProviders.ToList();
+
+            // Add the core resolvers
+            list.InsertRange(0, new BaseMetadataProvider[]{
+                new ImageFromMediaLocationProvider(),
+                new LocalTrailerProvider(),
+                new AudioInfoProvider(),
+                new FolderProviderFromXml()
+            });
+
+            MetadataProviders = list;
+
+            Parallel.ForEach(MetadataProviders, provider =>
+            {
+                provider.Init();
+            });
+        }
+
         /// <summary>
         /// <summary>
         /// Fires when a path is about to be resolved, but before child folders and files 
         /// Fires when a path is about to be resolved, but before child folders and files 
         /// have been collected from the file system.
         /// have been collected from the file system.
@@ -129,7 +165,7 @@ namespace MediaBrowser.Controller
         /// <summary>
         /// <summary>
         /// Reloads the root media folder
         /// Reloads the root media folder
         /// </summary>
         /// </summary>
-        public void ReloadRoot()
+        public async Task ReloadRoot()
         {
         {
             if (!Directory.Exists(MediaRootFolderPath))
             if (!Directory.Exists(MediaRootFolderPath))
             {
             {
@@ -138,7 +174,7 @@ namespace MediaBrowser.Controller
 
 
             DirectoryWatchers.Stop();
             DirectoryWatchers.Stop();
 
 
-            RootFolder = ItemController.GetItem(MediaRootFolderPath) as Folder;
+            RootFolder = await ItemController.GetItem(null, MediaRootFolderPath) as Folder;
 
 
             DirectoryWatchers.Start();
             DirectoryWatchers.Start();
         }
         }
@@ -152,23 +188,23 @@ namespace MediaBrowser.Controller
             }
             }
         }
         }
 
 
-        public void ReloadItem(BaseItem item)
+        public async Task ReloadItem(BaseItem item)
         {
         {
             Folder folder = item as Folder;
             Folder folder = item as Folder;
 
 
             if (folder != null && folder.IsRoot)
             if (folder != null && folder.IsRoot)
             {
             {
-                ReloadRoot();
+                await ReloadRoot();
             }
             }
             else
             else
             {
             {
                 if (!Directory.Exists(item.Path) && !File.Exists(item.Path))
                 if (!Directory.Exists(item.Path) && !File.Exists(item.Path))
                 {
                 {
-                    ReloadItem(item.Parent);
+                    await ReloadItem(item.Parent);
                     return;
                     return;
                 }
                 }
 
 
-                BaseItem newItem = ItemController.GetItem(item.Parent, item.Path);
+                BaseItem newItem = await ItemController.GetItem(item.Parent, item.Path);
 
 
                 List<BaseItem> children = item.Parent.Children.ToList();
                 List<BaseItem> children = item.Parent.Children.ToList();
 
 

+ 20 - 106
MediaBrowser.Controller/Library/ItemController.cs

@@ -3,8 +3,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
@@ -58,67 +56,12 @@ namespace MediaBrowser.Controller.Library
         }
         }
         #endregion
         #endregion
 
 
-        #region BaseItem Events
-        /// <summary>
-        /// Called when an item is being created.
-        /// This should be used to fill item values, such as metadata
-        /// </summary>
-        public event EventHandler<GenericItemEventArgs<BaseItem>> BaseItemCreating;
-
-        /// <summary>
-        /// Called when an item has been created.
-        /// This should be used to process or modify item values.
-        /// </summary>
-        public event EventHandler<GenericItemEventArgs<BaseItem>> BaseItemCreated;
-        #endregion
-
-        /// <summary>
-        /// Called when an item has been created
-        /// </summary>
-        private void OnBaseItemCreated(BaseItem item, Folder parent)
-        {
-            GenericItemEventArgs<BaseItem> args = new GenericItemEventArgs<BaseItem> { Item = item };
-
-            if (BaseItemCreating != null)
-            {
-                BaseItemCreating(this, args);
-            }
-
-            if (BaseItemCreated != null)
-            {
-                BaseItemCreated(this, args);
-            }
-        }
-
-        private void FireCreateEventsRecursive(Folder folder, Folder parent)
-        {
-            OnBaseItemCreated(folder, parent);
-
-            int count = folder.Children.Length;
-
-            Parallel.For(0, count, i =>
-            {
-                BaseItem item = folder.Children[i];
-
-                Folder childFolder = item as Folder;
-
-                if (childFolder != null)
-                {
-                    FireCreateEventsRecursive(childFolder, folder);
-                }
-                else
-                {
-                    OnBaseItemCreated(item, folder);
-                }
-            });
-        }
-
-        private BaseItem ResolveItem(ItemResolveEventArgs args)
+        private async Task<BaseItem> ResolveItem(ItemResolveEventArgs args)
         {
         {
             // If that didn't pan out, try the slow ones
             // If that didn't pan out, try the slow ones
             foreach (IBaseItemResolver resolver in Kernel.Instance.EntityResolvers)
             foreach (IBaseItemResolver resolver in Kernel.Instance.EntityResolvers)
             {
             {
-                var item = resolver.ResolvePath(args);
+                var item = await resolver.ResolvePath(args);
 
 
                 if (item != null)
                 if (item != null)
                 {
                 {
@@ -132,39 +75,15 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// <summary>
         /// Resolves a path into a BaseItem
         /// Resolves a path into a BaseItem
         /// </summary>
         /// </summary>
-        public BaseItem GetItem(string path)
-        {
-            return GetItem(null, path);
-        }
-
-        /// <summary>
-        /// Resolves a path into a BaseItem
-        /// </summary>
-        public BaseItem GetItem(Folder parent, string path)
+        public async Task<BaseItem> GetItem(Folder parent, string path)
         {
         {
-            BaseItem item = GetItemInternal(parent, path, File.GetAttributes(path));
-
-            if (item != null)
-            {
-                var folder = item as Folder;
-
-                if (folder != null)
-                {
-                    FireCreateEventsRecursive(folder, parent);
-                }
-                else
-                {
-                    OnBaseItemCreated(item, parent);
-                }
-            }
-
-            return item;
+            return await GetItemInternal(parent, path, File.GetAttributes(path));
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Resolves a path into a BaseItem
         /// Resolves a path into a BaseItem
         /// </summary>
         /// </summary>
-        private BaseItem GetItemInternal(Folder parent, string path, FileAttributes attributes)
+        private async Task<BaseItem> GetItemInternal(Folder parent, string path, FileAttributes attributes)
         {
         {
             if (!OnPreBeginResolvePath(parent, path, attributes))
             if (!OnPreBeginResolvePath(parent, path, attributes))
             {
             {
@@ -201,14 +120,14 @@ namespace MediaBrowser.Controller.Library
                 return null;
                 return null;
             }
             }
 
 
-            BaseItem item = ResolveItem(args);
+            BaseItem item = await ResolveItem(args);
 
 
             var folder = item as Folder;
             var folder = item as Folder;
 
 
             if (folder != null)
             if (folder != null)
             {
             {
                 // If it's a folder look for child entities
                 // If it's a folder look for child entities
-                AttachChildren(folder, fileSystemChildren);
+                await AttachChildren(folder, fileSystemChildren);
             }
             }
 
 
             return item;
             return item;
@@ -217,30 +136,25 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// <summary>
         /// Finds child BaseItems for a given Folder
         /// Finds child BaseItems for a given Folder
         /// </summary>
         /// </summary>
-        private void AttachChildren(Folder folder, IEnumerable<KeyValuePair<string, FileAttributes>> fileSystemChildren)
+        private async Task AttachChildren(Folder folder, IEnumerable<KeyValuePair<string, FileAttributes>> fileSystemChildren)
         {
         {
-            List<BaseItem> baseItemChildren = new List<BaseItem>();
+            KeyValuePair<string, FileAttributes>[] fileSystemChildrenArray = fileSystemChildren.ToArray();
 
 
-            int count = fileSystemChildren.Count();
+            int count = fileSystemChildrenArray.Length;
 
 
-            // Resolve the child folder paths into entities
-            Parallel.For(0, count, i =>
-            {
-                KeyValuePair<string, FileAttributes> child = fileSystemChildren.ElementAt(i);
+            Task<BaseItem>[] tasks = new Task<BaseItem>[count];
 
 
-                BaseItem item = GetItemInternal(folder, child.Key, child.Value);
+            for (int i = 0; i < count; i++)
+            {
+                var child = fileSystemChildrenArray[i];
 
 
-                if (item != null)
-                {
-                    lock (baseItemChildren)
-                    {
-                        baseItemChildren.Add(item);
-                    }
-                }
-            });
+                tasks[i] = GetItemInternal(folder, child.Key, child.Value);
+            }
 
 
+            BaseItem[] baseItemChildren = await Task<BaseItem>.WhenAll(tasks);
+            
             // Sort them
             // Sort them
-            folder.Children = baseItemChildren.OrderBy(f =>
+            folder.Children = baseItemChildren.Where(i => i != null).OrderBy(f =>
             {
             {
                 return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
                 return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
 
 
@@ -363,7 +277,7 @@ namespace MediaBrowser.Controller.Library
         /// Creates an IBN item based on a given path
         /// Creates an IBN item based on a given path
         /// </summary>
         /// </summary>
         private T CreateImagesByNameItem<T>(string path, string name)
         private T CreateImagesByNameItem<T>(string path, string name)
-            where T : BaseEntity, new ()
+            where T : BaseEntity, new()
         {
         {
             T item = new T();
             T item = new T();
 
 

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

@@ -59,6 +59,11 @@
     <Compile Include="Library\ItemController.cs" />
     <Compile Include="Library\ItemController.cs" />
     <Compile Include="Kernel.cs" />
     <Compile Include="Kernel.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Providers\BaseMetadataProvider.cs" />
+    <Compile Include="Providers\AudioInfoProvider.cs" />
+    <Compile Include="Providers\FolderProviderFromXml.cs" />
+    <Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
+    <Compile Include="Providers\LocalTrailerProvider.cs" />
     <Compile Include="Resolvers\AudioResolver.cs" />
     <Compile Include="Resolvers\AudioResolver.cs" />
     <Compile Include="Resolvers\BaseItemResolver.cs" />
     <Compile Include="Resolvers\BaseItemResolver.cs" />
     <Compile Include="Resolvers\FolderResolver.cs" />
     <Compile Include="Resolvers\FolderResolver.cs" />

+ 86 - 0
MediaBrowser.Controller/Providers/AudioInfoProvider.cs

@@ -0,0 +1,86 @@
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.FFMpeg;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+    [Export(typeof(BaseMetadataProvider))]
+    public class AudioInfoProvider : BaseMetadataProvider
+    {
+        public override bool Supports(BaseItem item)
+        {
+            return item is Audio;
+        }
+
+        public async override Task Fetch(BaseItem item, ItemResolveEventArgs args)
+        {
+            Audio audio = item as Audio;
+
+            string outputDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, item.Id.ToString().Substring(0, 1));
+
+            string outputPath = Path.Combine(outputDirectory, item.Id + "-" + item.DateModified.Ticks + ".js");
+
+            FFProbeResult data = await FFProbe.Run(audio, outputPath);
+
+            MediaStream stream = data.streams.FirstOrDefault(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
+
+            audio.Channels = stream.channels;
+
+            string bitrate = null;
+            
+            if (!string.IsNullOrEmpty(stream.sample_rate))
+            {
+                audio.SampleRate = int.Parse(stream.sample_rate);
+
+                bitrate = stream.bit_rate;
+            }
+
+            if (string.IsNullOrEmpty(bitrate))
+            {
+                bitrate = data.format.bit_rate;
+            }
+
+            if (!string.IsNullOrEmpty(bitrate))
+            {
+                audio.BitRate = int.Parse(bitrate);
+            }
+        }
+
+        private string GetOutputCachePath(BaseItem item)
+        {
+            string outputDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, item.Id.ToString().Substring(0, 1));
+
+            return Path.Combine(outputDirectory, item.Id + "-" + item.DateModified.Ticks + ".js");
+        }
+
+        public override void Init()
+        {
+            base.Init();
+
+            for (int i = 0; i <= 9; i++)
+            {
+                EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, i.ToString()));
+            }
+
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "a"));
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "b"));
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "c"));
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "d"));
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "e"));
+            EnsureDirectory(Path.Combine(Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory, "f"));
+        }
+
+        private void EnsureDirectory(string path)
+        {
+            if (!Directory.Exists(path))
+            {
+                Directory.CreateDirectory(path);
+            }
+        }
+    }
+}

+ 23 - 0
MediaBrowser.Controller/Providers/BaseMetadataProvider.cs

@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public abstract class BaseMetadataProvider
+    {
+        /// <summary>
+        /// If the provider needs any startup routines, add them here
+        /// </summary>
+        public virtual void Init()
+        {
+        }
+
+        public virtual bool Supports(BaseItem item)
+        {
+            return true;
+        }
+
+        public abstract Task Fetch(BaseItem item, ItemResolveEventArgs args);
+    }
+}

+ 30 - 0
MediaBrowser.Controller/Providers/FolderProviderFromXml.cs

@@ -0,0 +1,30 @@
+using System.ComponentModel.Composition;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Xml;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+    [Export(typeof(BaseMetadataProvider))]
+    public class FolderProviderFromXml : BaseMetadataProvider
+    {
+        public override bool Supports(BaseItem item)
+        {
+            return item is Folder;
+        }
+
+        public override Task Fetch(BaseItem item, ItemResolveEventArgs args)
+        {
+            return Task.Run(() =>
+            {
+                var metadataFile = args.GetFileByName("folder.xml");
+
+                if (metadataFile.HasValue)
+                {
+                    new FolderXmlParser().Fetch(item as Folder, metadataFile.Value.Key);
+                }
+            });
+        }
+    }
+}

+ 85 - 0
MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs

@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+    [Export(typeof(BaseMetadataProvider))]
+    public class ImageFromMediaLocationProvider : BaseMetadataProvider
+    {
+        public override Task Fetch(BaseItem item, ItemResolveEventArgs args)
+        {
+            return Task.Run(() =>
+            {
+                if (args.IsFolder)
+                {
+                    PopulateImages(item, args);
+                }
+            });
+        }
+
+        /// <summary>
+        /// Fills in image paths based on files win the folder
+        /// </summary>
+        private void PopulateImages(BaseItem item, ItemResolveEventArgs args)
+        {
+            List<string> backdropFiles = new List<string>();
+
+            foreach (KeyValuePair<string, FileAttributes> file in args.FileSystemChildren)
+            {
+                if (file.Value.HasFlag(FileAttributes.Directory))
+                {
+                    continue;
+                }
+
+                string filePath = file.Key;
+
+                string ext = Path.GetExtension(filePath);
+
+                // Only support png and jpg files
+                if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
+                string name = Path.GetFileNameWithoutExtension(filePath);
+
+                if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.PrimaryImagePath = filePath;
+                }
+                else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
+                {
+                    backdropFiles.Add(filePath);
+                }
+                if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.LogoImagePath = filePath;
+                }
+                if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.BannerImagePath = filePath;
+                }
+                if (name.Equals("art", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.ArtImagePath = filePath;
+                }
+                if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.ThumbnailImagePath = filePath;
+                }
+            }
+
+            if (backdropFiles.Any())
+            {
+                item.BackdropImagePaths = backdropFiles;
+            }
+        }
+
+    }
+}

+ 39 - 0
MediaBrowser.Controller/Providers/LocalTrailerProvider.cs

@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+    [Export(typeof(BaseMetadataProvider))]
+    public class LocalTrailerProvider : BaseMetadataProvider
+    {
+        public async override Task Fetch(BaseItem item, ItemResolveEventArgs args)
+        {
+            var trailerPath = args.GetFolderByName("trailers");
+
+            if (trailerPath.HasValue)
+            {
+                string[] allFiles = Directory.GetFileSystemEntries(trailerPath.Value.Key, "*", SearchOption.TopDirectoryOnly);
+
+                List<Video> localTrailers = new List<Video>();
+
+                foreach (string file in allFiles)
+                {
+                    BaseItem child = await Kernel.Instance.ItemController.GetItem(null, file);
+
+                    Video video = child as Video;
+
+                    if (video != null)
+                    {
+                        localTrailers.Add(video);
+                    }
+                }
+
+                item.LocalTrailers = localTrailers;
+            }
+        }
+    }
+}

+ 17 - 78
MediaBrowser.Controller/Resolvers/BaseItemResolver.cs

@@ -1,8 +1,8 @@
 using System;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.IO;
-using System.Linq;
+using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.Resolvers
 namespace MediaBrowser.Controller.Resolvers
@@ -33,19 +33,15 @@ namespace MediaBrowser.Controller.Resolvers
             }
             }
 
 
             item.Id = Kernel.GetMD5(item.Path);
             item.Id = Kernel.GetMD5(item.Path);
-            
-            PopulateImages(item, args);
-            PopulateLocalTrailers(item, args);
         }
         }
 
 
-        public BaseItem ResolvePath(ItemResolveEventArgs args)
+        public async Task<BaseItem> ResolvePath(ItemResolveEventArgs args)
         {
         {
             T item = Resolve(args);
             T item = Resolve(args);
             
             
             if (item != null)
             if (item != null)
             {
             {
                 // Set initial values on the newly resolved item
                 // Set initial values on the newly resolved item
-                
                 SetItemValues(item, args);
                 SetItemValues(item, args);
 
 
                 // Make sure the item has a name
                 // Make sure the item has a name
@@ -53,11 +49,24 @@ namespace MediaBrowser.Controller.Resolvers
 
 
                 // Make sure DateCreated and DateModified have values
                 // Make sure DateCreated and DateModified have values
                 EnsureDates(item);
                 EnsureDates(item);
+                
+                await FetchMetadataFromProviders(item, args);
             }
             }
 
 
             return item;
             return item;
         }
         }
 
 
+        private async Task FetchMetadataFromProviders(T item, ItemResolveEventArgs args)
+        {
+            foreach (BaseMetadataProvider provider in Kernel.Instance.MetadataProviders)
+            {
+                if (provider.Supports(item))
+                {
+                    await provider.Fetch(item, args);
+                }
+            }
+        }
+
         private void EnsureName(T item)
         private void EnsureName(T item)
         {
         {
             // If the subclass didn't supply a name, add it here
             // If the subclass didn't supply a name, add it here
@@ -84,76 +93,6 @@ namespace MediaBrowser.Controller.Resolvers
                 item.DateModified = Path.IsPathRooted(item.Path) ? File.GetLastWriteTime(item.Path) : DateTime.Now;
                 item.DateModified = Path.IsPathRooted(item.Path) ? File.GetLastWriteTime(item.Path) : DateTime.Now;
             }
             }
         }
         }
-
-        /// <summary>
-        /// Fills in image paths based on files win the folder
-        /// </summary>
-        protected virtual void PopulateImages(T item, ItemResolveEventArgs args)
-        {
-            List<string> backdropFiles = new List<string>();
-
-            foreach (KeyValuePair<string,FileAttributes> file in args.FileSystemChildren)
-            {
-                if (file.Value.HasFlag(FileAttributes.Directory))
-                {
-                    continue;
-                }
-
-                string filePath = file.Key;
-
-                string ext = Path.GetExtension(filePath);
-
-                // Only support png and jpg files
-                if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
-
-                string name = Path.GetFileNameWithoutExtension(filePath);
-
-                if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
-                {
-                    item.PrimaryImagePath = filePath;
-                }
-                else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
-                {
-                    backdropFiles.Add(filePath);
-                }
-                if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
-                {
-                    item.LogoImagePath = filePath;
-                }
-                if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
-                {
-                    item.BannerImagePath = filePath;
-                }
-                if (name.Equals("art", StringComparison.OrdinalIgnoreCase))
-                {
-                    item.ArtImagePath = filePath;
-                }
-                if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
-                {
-                    item.ThumbnailImagePath = filePath;
-                }
-            }
-
-            if (backdropFiles.Any())
-            {
-                item.BackdropImagePaths = backdropFiles;
-            }
-        }
-
-        protected virtual void PopulateLocalTrailers(T item, ItemResolveEventArgs args)
-        {
-            var trailerPath = args.GetFolderByName("trailers");
-
-            if (trailerPath.HasValue)
-            {
-                string[] allFiles = Directory.GetFileSystemEntries(trailerPath.Value.Key, "*", SearchOption.TopDirectoryOnly);
-
-                item.LocalTrailers = allFiles.Select(f => Kernel.Instance.ItemController.GetItem(f)).OfType<Video>();
-            }
-        }
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -161,6 +100,6 @@ namespace MediaBrowser.Controller.Resolvers
     /// </summary>
     /// </summary>
     public interface IBaseItemResolver
     public interface IBaseItemResolver
     {
     {
-        BaseItem ResolvePath(ItemResolveEventArgs args);
+        Task<BaseItem> ResolvePath(ItemResolveEventArgs args);
     }
     }
 }
 }

+ 0 - 14
MediaBrowser.Controller/Resolvers/FolderResolver.cs

@@ -1,6 +1,5 @@
 using System.ComponentModel.Composition;
 using System.ComponentModel.Composition;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
-using MediaBrowser.Controller.Xml;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.Resolvers
 namespace MediaBrowser.Controller.Resolvers
@@ -27,19 +26,6 @@ namespace MediaBrowser.Controller.Resolvers
             base.SetItemValues(item, args);
             base.SetItemValues(item, args);
 
 
             item.IsRoot = args.Parent == null;
             item.IsRoot = args.Parent == null;
-
-            // Read data from folder.xml, if it exists
-            PopulateFolderMetadata(item, args);
-        }
-
-        private void PopulateFolderMetadata(TItemType folder, ItemResolveEventArgs args)
-        {
-            var metadataFile = args.GetFileByName("folder.xml");
-
-            if (metadataFile.HasValue)
-            {
-                new FolderXmlParser().Fetch(folder, metadataFile.Value.Key);
-            }
         }
         }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Movies/Resolvers/MovieResolver.cs

@@ -85,7 +85,7 @@ namespace MediaBrowser.Movies.Resolvers
             {
             {
                 string[] allFiles = Directory.GetFileSystemEntries(trailerPath.Value.Key, "*", SearchOption.TopDirectoryOnly);
                 string[] allFiles = Directory.GetFileSystemEntries(trailerPath.Value.Key, "*", SearchOption.TopDirectoryOnly);
 
 
-                item.SpecialFeatures = allFiles.Select(f => Kernel.Instance.ItemController.GetItem(f)).OfType<Video>();
+                item.SpecialFeatures = allFiles.Select(f => Kernel.Instance.ItemController.GetItem(null, f)).OfType<Video>();
             }
             }
         }
         }
 
 

+ 1 - 1
MediaBrowser.ServerApplication/MainWindow.xaml

@@ -2,7 +2,7 @@
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:tb="http://www.hardcodet.net/taskbar"
         xmlns:tb="http://www.hardcodet.net/taskbar"
-        Title="MainWindow" Height="350" Width="525" AllowsTransparency="True" Background="Transparent" WindowStyle="None" ShowInTaskbar="False" Loaded="MainWindow_Loaded">
+        Title="MainWindow" Height="350" Width="525" AllowsTransparency="True" Background="Transparent" WindowStyle="None" ShowInTaskbar="False">
     <Grid>
     <Grid>
         <tb:TaskbarIcon Name="MbTaskbarIcon" IconSource="/Icons/Icon.ico" ToolTipText="MediaBrowser Server" Visibility="Hidden">
         <tb:TaskbarIcon Name="MbTaskbarIcon" IconSource="/Icons/Icon.ico" ToolTipText="MediaBrowser Server" Visibility="Hidden">
 
 

+ 8 - 13
MediaBrowser.ServerApplication/MainWindow.xaml.cs

@@ -4,6 +4,8 @@ using System.Windows;
 using MediaBrowser.Common.Logging;
 using MediaBrowser.Common.Logging;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Model.Progress;
 using MediaBrowser.Model.Progress;
+using System.Threading.Tasks;
+using MediaBrowser.Common.UI;
 
 
 namespace MediaBrowser.ServerApplication
 namespace MediaBrowser.ServerApplication
 {
 {
@@ -18,10 +20,10 @@ namespace MediaBrowser.ServerApplication
             LoadKernel();
             LoadKernel();
         }
         }
 
 
-        private void LoadKernel()
+        private async void LoadKernel()
         {
         {
             Progress<TaskProgress> progress = new Progress<TaskProgress>();
             Progress<TaskProgress> progress = new Progress<TaskProgress>();
-            Common.UI.Splash splash = new Common.UI.Splash(progress);
+            Splash splash = new Splash(progress);
 
 
             splash.Show();
             splash.Show();
             
             
@@ -29,11 +31,14 @@ namespace MediaBrowser.ServerApplication
             {
             {
                 DateTime now = DateTime.Now;
                 DateTime now = DateTime.Now;
 
 
-                new Kernel().Init(progress);
+                await new Kernel().Init(progress);
 
 
                 double seconds = (DateTime.Now - now).TotalSeconds;
                 double seconds = (DateTime.Now - now).TotalSeconds;
 
 
                 Logger.LogInfo("Kernel.Init completed in {0} seconds.", seconds);
                 Logger.LogInfo("Kernel.Init completed in {0} seconds.", seconds);
+
+                // Don't show the system tray icon until the kernel finishes.
+                this.MbTaskbarIcon.Visibility = System.Windows.Visibility.Visible;
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -46,16 +51,6 @@ namespace MediaBrowser.ServerApplication
             }
             }
         }
         }
 
 
-        #region Main Window Events
-
-        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
-        {
-            // Don't show the system tray icon until the app has loaded.
-            this.MbTaskbarIcon.Visibility = System.Windows.Visibility.Visible;
-        }
-
-        #endregion
-
         #region Context Menu events
         #region Context Menu events
 
 
         private void cmOpenDashboard_click(object sender, RoutedEventArgs e)
         private void cmOpenDashboard_click(object sender, RoutedEventArgs e)