From 767cdc1f6f6a63ce997fc9476911e2c361f9d402 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Wed, 20 Feb 2013 20:33:05 -0500 Subject: Pushing missing changes --- MediaBrowser.Controller/Library/LibraryManager.cs | 511 ++++++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 MediaBrowser.Controller/Library/LibraryManager.cs (limited to 'MediaBrowser.Controller/Library/LibraryManager.cs') diff --git a/MediaBrowser.Controller/Library/LibraryManager.cs b/MediaBrowser.Controller/Library/LibraryManager.cs new file mode 100644 index 000000000..7d3c764b2 --- /dev/null +++ b/MediaBrowser.Controller/Library/LibraryManager.cs @@ -0,0 +1,511 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Win32; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Tasks; +using MoreLinq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Class LibraryManager + /// + public class LibraryManager : BaseManager + { + #region LibraryChanged Event + /// + /// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args. + /// *** Will fire asynchronously. *** + /// + public event EventHandler LibraryChanged; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + internal void OnLibraryChanged(ChildrenChangedEventArgs args) + { + EventHelper.QueueEventIfNotNull(LibraryChanged, this, args); + + // Had to put this in a separate method to avoid an implicitly captured closure + SendLibraryChangedWebSocketMessage(args); + } + + /// + /// Sends the library changed web socket message. + /// + /// The instance containing the event data. + private void SendLibraryChangedWebSocketMessage(ChildrenChangedEventArgs args) + { + // Notify connected ui's + Kernel.TcpManager.SendWebSocketMessage("LibraryChanged", () => DtoBuilder.GetLibraryUpdateInfo(args)); + } + #endregion + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public LibraryManager(Kernel kernel) + : base(kernel) + { + } + + /// + /// Resolves the item. + /// + /// The args. + /// BaseItem. + public BaseItem ResolveItem(ItemResolveArgs args) + { + return Kernel.EntityResolvers.Select(r => r.ResolvePath(args)).FirstOrDefault(i => i != null); + } + + /// + /// Resolves a path into a BaseItem + /// + /// The path. + /// The parent. + /// The file info. + /// BaseItem. + /// + public BaseItem GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + fileInfo = fileInfo ?? FileSystem.GetFileData(path); + + if (!fileInfo.HasValue) + { + return null; + } + + var args = new ItemResolveArgs + { + Parent = parent, + Path = path, + FileInfo = fileInfo.Value + }; + + // Return null if ignore rules deem that we should do so + if (Kernel.EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args))) + { + return null; + } + + // Gather child folder and files + if (args.IsDirectory) + { + // When resolving the root, we need it's grandchildren (children of user views) + var flattenFolderDepth = args.IsPhysicalRoot ? 2 : 0; + + args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, flattenFolderDepth: flattenFolderDepth, args: args); + } + + // Check to see if we should resolve based on our contents + if (args.IsDirectory && !EntityResolutionHelper.ShouldResolvePathContents(args)) + { + return null; + } + + return ResolveItem(args); + } + + /// + /// Resolves a set of files into a list of BaseItem + /// + /// + /// The files. + /// The parent. + /// List{``0}. + public List GetItems(IEnumerable files, Folder parent) + where T : BaseItem + { + var list = new List(); + + Parallel.ForEach(files, f => + { + try + { + var item = GetItem(f.Path, parent, f) as T; + + if (item != null) + { + lock (list) + { + list.Add(item); + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error resolving path {0}", ex, f.Path); + } + }); + + return list; + } + + /// + /// Creates the root media folder + /// + /// AggregateFolder. + /// Cannot create the root folder until plugins have loaded + internal AggregateFolder CreateRootFolder() + { + if (Kernel.Plugins == null) + { + throw new InvalidOperationException("Cannot create the root folder until plugins have loaded"); + } + + var rootFolderPath = Kernel.ApplicationPaths.RootFolderPath; + var rootFolder = Kernel.ItemRepository.RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)GetItem(rootFolderPath); + + // Add in the plug-in folders + foreach (var child in Kernel.PluginFolders) + { + rootFolder.AddVirtualChild(child); + } + + return rootFolder; + } + + /// + /// Gets a Person + /// + /// The name. + /// if set to true [allow slow providers]. + /// Task{Person}. + public Task GetPerson(string name, bool allowSlowProviders = false) + { + return GetPerson(name, CancellationToken.None, allowSlowProviders); + } + + /// + /// Gets a Person + /// + /// The name. + /// The cancellation token. + /// if set to true [allow slow providers]. + /// Task{Person}. + private Task GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false) + { + return GetImagesByNameItem(Kernel.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders); + } + + /// + /// Gets a Studio + /// + /// The name. + /// if set to true [allow slow providers]. + /// Task{Studio}. + public Task GetStudio(string name, bool allowSlowProviders = false) + { + return GetImagesByNameItem(Kernel.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders); + } + + /// + /// Gets a Genre + /// + /// The name. + /// if set to true [allow slow providers]. + /// Task{Genre}. + public Task GetGenre(string name, bool allowSlowProviders = false) + { + return GetImagesByNameItem(Kernel.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders); + } + + /// + /// The us culture + /// + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Gets a Year + /// + /// The value. + /// if set to true [allow slow providers]. + /// Task{Year}. + /// + public Task GetYear(int value, bool allowSlowProviders = false) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(); + } + + return GetImagesByNameItem(Kernel.ApplicationPaths.YearPath, value.ToString(UsCulture), CancellationToken.None, allowSlowProviders); + } + + /// + /// The images by name item cache + /// + private readonly ConcurrentDictionary ImagesByNameItemCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Generically retrieves an IBN item + /// + /// + /// The path. + /// The name. + /// The cancellation token. + /// if set to true [allow slow providers]. + /// Task{``0}. + /// + private Task GetImagesByNameItem(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true) + where T : BaseItem, new() + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(); + } + + var key = Path.Combine(path, FileSystem.GetValidFilename(name)); + + var obj = ImagesByNameItemCache.GetOrAdd(key, keyname => CreateImagesByNameItem(path, name, cancellationToken, allowSlowProviders)); + + return obj as Task; + } + + /// + /// Creates an IBN item based on a given path + /// + /// + /// The path. + /// The name. + /// The cancellation token. + /// if set to true [allow slow providers]. + /// Task{``0}. + /// Path not created: + path + private async Task CreateImagesByNameItem(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true) + where T : BaseItem, new() + { + cancellationToken.ThrowIfCancellationRequested(); + + Logger.Debug("Creating {0}: {1}", typeof(T).Name, name); + + path = Path.Combine(path, FileSystem.GetValidFilename(name)); + + var fileInfo = FileSystem.GetFileData(path); + + var isNew = false; + + if (!fileInfo.HasValue) + { + Directory.CreateDirectory(path); + fileInfo = FileSystem.GetFileData(path); + + if (!fileInfo.HasValue) + { + throw new IOException("Path not created: " + path); + } + + isNew = true; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var id = path.GetMBId(typeof(T)); + + var item = Kernel.ItemRepository.RetrieveItem(id) as T; + if (item == null) + { + item = new T + { + Name = name, + Id = id, + DateCreated = fileInfo.Value.CreationTimeUtc, + DateModified = fileInfo.Value.LastWriteTimeUtc, + Path = path + }; + isNew = true; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Set this now so we don't cause additional file system access during provider executions + item.ResetResolveArgs(fileInfo); + + await item.RefreshMetadata(cancellationToken, isNew, allowSlowProviders: allowSlowProviders).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + return item; + } + + /// + /// Validate and refresh the People sub-set of the IBN. + /// The items are stored in the db but not loaded into memory until actually requested by an operation. + /// + /// The cancellation token. + /// The progress. + /// Task. + internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) + { + // Clear the IBN cache + ImagesByNameItemCache.Clear(); + + const int maxTasks = 250; + + var tasks = new List(); + + var includedPersonTypes = new[] { PersonType.Actor, PersonType.Director }; + + var people = Kernel.RootFolder.RecursiveChildren + .Where(c => c.People != null) + .SelectMany(c => c.People.Where(p => includedPersonTypes.Contains(p.Type))) + .DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var numComplete = 0; + + foreach (var person in people) + { + if (tasks.Count > maxTasks) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + + // Safe cancellation point, when there are no pending tasks + cancellationToken.ThrowIfCancellationRequested(); + } + + // Avoid accessing the foreach variable within the closure + var currentPerson = person; + + tasks.Add(Task.Run(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + await GetPerson(currentPerson.Name, cancellationToken, allowSlowProviders: true).ConfigureAwait(false); + } + catch (IOException ex) + { + Logger.ErrorException("Error validating IBN entry {0}", ex, currentPerson.Name); + } + + // Update progress + lock (progress) + { + numComplete++; + double percent = numComplete; + percent /= people.Count; + + progress.Report(new TaskProgress { PercentComplete = 100 * percent }); + } + })); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + progress.Report(new TaskProgress { PercentComplete = 100 }); + + Logger.Info("People validation complete"); + } + + /// + /// Reloads the root media folder + /// + /// The progress. + /// The cancellation token. + /// Task. + internal async Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) + { + Logger.Info("Validating media library"); + + await Kernel.RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + + // Start by just validating the children of the root, but go no further + await Kernel.RootFolder.ValidateChildren(new Progress { }, cancellationToken, recursive: false); + + // Validate only the collection folders for each user, just to make them available as quickly as possible + var userCollectionFolderTasks = Kernel.Users.AsParallel().Select(user => user.ValidateCollectionFolders(new Progress { }, cancellationToken)); + await Task.WhenAll(userCollectionFolderTasks).ConfigureAwait(false); + + // Now validate the entire media library + await Kernel.RootFolder.ValidateChildren(progress, cancellationToken, recursive: true).ConfigureAwait(false); + + foreach (var user in Kernel.Users) + { + await user.ValidateMediaLibrary(new Progress { }, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Saves display preferences for a Folder + /// + /// The user. + /// The folder. + /// The data. + /// Task. + public Task SaveDisplayPreferencesForFolder(User user, Folder folder, DisplayPreferences data) + { + // Need to update all items with the same DisplayPrefsId + foreach (var child in Kernel.RootFolder.GetRecursiveChildren(user) + .OfType() + .Where(i => i.DisplayPrefsId == folder.DisplayPrefsId)) + { + child.AddOrUpdateDisplayPrefs(user, data); + } + + return Kernel.DisplayPreferencesRepository.SaveDisplayPrefs(folder, CancellationToken.None); + } + + /// + /// Gets the default view. + /// + /// IEnumerable{VirtualFolderInfo}. + public IEnumerable GetDefaultVirtualFolders() + { + return GetView(Kernel.ApplicationPaths.DefaultUserViewsPath); + } + + /// + /// Gets the view. + /// + /// The user. + /// IEnumerable{VirtualFolderInfo}. + public IEnumerable GetVirtualFolders(User user) + { + return GetView(user.RootFolderPath); + } + + /// + /// Gets the view. + /// + /// The path. + /// IEnumerable{VirtualFolderInfo}. + private IEnumerable GetView(string path) + { + return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly) + .Select(dir => new VirtualFolderInfo + { + Name = Path.GetFileName(dir), + Locations = Directory.EnumerateFiles(dir, "*.lnk", SearchOption.TopDirectoryOnly).Select(FileSystem.ResolveShortcut).ToList() + }); + } + } +} -- cgit v1.2.3