From 767cdc1f6f6a63ce997fc9476911e2c361f9d402 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Wed, 20 Feb 2013 20:33:05 -0500 Subject: Pushing missing changes --- .../Library/ChildrenChangedEventArgs.cs | 171 +++- MediaBrowser.Controller/Library/DtoBuilder.cs | 934 +++++++++++++++++++++ MediaBrowser.Controller/Library/ItemController.cs | 136 --- MediaBrowser.Controller/Library/ItemResolveArgs.cs | 397 +++++++++ .../Library/ItemResolveEventArgs.cs | 104 --- MediaBrowser.Controller/Library/LibraryManager.cs | 511 +++++++++++ MediaBrowser.Controller/Library/Profiler.cs | 69 ++ MediaBrowser.Controller/Library/ResourcePool.cs | 79 ++ MediaBrowser.Controller/Library/UserDataManager.cs | 219 +++++ MediaBrowser.Controller/Library/UserManager.cs | 395 +++++++++ 10 files changed, 2741 insertions(+), 274 deletions(-) create mode 100644 MediaBrowser.Controller/Library/DtoBuilder.cs delete mode 100644 MediaBrowser.Controller/Library/ItemController.cs create mode 100644 MediaBrowser.Controller/Library/ItemResolveArgs.cs delete mode 100644 MediaBrowser.Controller/Library/ItemResolveEventArgs.cs create mode 100644 MediaBrowser.Controller/Library/LibraryManager.cs create mode 100644 MediaBrowser.Controller/Library/Profiler.cs create mode 100644 MediaBrowser.Controller/Library/ResourcePool.cs create mode 100644 MediaBrowser.Controller/Library/UserDataManager.cs create mode 100644 MediaBrowser.Controller/Library/UserManager.cs (limited to 'MediaBrowser.Controller/Library') diff --git a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs index 462fcc6d69..94f4c540ff 100644 --- a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs +++ b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs @@ -1,34 +1,137 @@ -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 ItemsAdded { get; set; } - public IEnumerable ItemsRemoved { get; set; } - - public ChildrenChangedEventArgs() - { - //initialize the list - ItemsAdded = new List(); - } - - /// - /// Create the args and set the folder property - /// - /// - public ChildrenChangedEventArgs(Folder folder) - { - //init the folder property - this.Folder = folder; - //init the list - ItemsAdded = new List(); - } - } -} +using System.Collections.Concurrent; +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Class ChildrenChangedEventArgs + /// + public class ChildrenChangedEventArgs : EventArgs + { + /// + /// Gets or sets the folder. + /// + /// The folder. + public Folder Folder { get; set; } + /// + /// Gets or sets the items added. + /// + /// The items added. + public ConcurrentBag ItemsAdded { get; set; } + /// + /// Gets or sets the items removed. + /// + /// The items removed. + public List ItemsRemoved { get; set; } + /// + /// Gets or sets the items updated. + /// + /// The items updated. + public ConcurrentBag ItemsUpdated { get; set; } + + /// + /// Create the args and set the folder property + /// + /// The folder. + /// + public ChildrenChangedEventArgs(Folder folder) + { + if (folder == null) + { + throw new ArgumentNullException(); + } + + //init the folder property + Folder = folder; + //init the list + ItemsAdded = new ConcurrentBag(); + ItemsRemoved = new List(); + ItemsUpdated = new ConcurrentBag(); + } + + /// + /// Adds the new item. + /// + /// The item. + /// + public void AddNewItem(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + ItemsAdded.Add(item); + } + + /// + /// Adds the updated item. + /// + /// The item. + /// + public void AddUpdatedItem(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + ItemsUpdated.Add(item); + } + + /// + /// Adds the removed item. + /// + /// The item. + /// + public void AddRemovedItem(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + ItemsRemoved.Add(item); + } + + /// + /// Lists the has change. + /// + /// The list. + /// true if XXXX, false otherwise + private bool ListHasChange(List list) + { + return list != null && list.Count > 0; + } + + /// + /// Lists the has change. + /// + /// The list. + /// true if XXXX, false otherwise + private bool ListHasChange(ConcurrentBag list) + { + return list != null && !list.IsEmpty; + } + + /// + /// Gets a value indicating whether this instance has change. + /// + /// true if this instance has change; otherwise, false. + public bool HasChange + { + get { return HasAddOrRemoveChange || ListHasChange(ItemsUpdated); } + } + + /// + /// Gets a value indicating whether this instance has add or remove change. + /// + /// true if this instance has add or remove change; otherwise, false. + public bool HasAddOrRemoveChange + { + get { return ListHasChange(ItemsAdded) || ListHasChange(ItemsRemoved); } + } + } +} diff --git a/MediaBrowser.Controller/Library/DtoBuilder.cs b/MediaBrowser.Controller/Library/DtoBuilder.cs new file mode 100644 index 0000000000..b16671e9e2 --- /dev/null +++ b/MediaBrowser.Controller/Library/DtoBuilder.cs @@ -0,0 +1,934 @@ +using MediaBrowser.Common.Logging; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.DTO; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Generates DTO's from domain entities + /// + public static class DtoBuilder + { + /// + /// The index folder delimeter + /// + const string IndexFolderDelimeter = "-index-"; + + /// + /// Gets the dto base item. + /// + /// The item. + /// The fields. + /// Task{DtoBaseItem}. + /// item + public async static Task GetDtoBaseItem(BaseItem item, List fields) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + if (fields == null) + { + throw new ArgumentNullException("fields"); + } + + var dto = new DtoBaseItem(); + + var tasks = new List(); + + if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) + { + try + { + tasks.Add(AttachPrimaryImageAspectRatio(dto, item)); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name); + } + } + + if (fields.Contains(ItemFields.Studios)) + { + dto.Studios = item.Studios; + } + + if (fields.Contains(ItemFields.People)) + { + tasks.Add(AttachPeople(dto, item)); + } + + AttachBasicFields(dto, item, fields); + + // Make sure all the tasks we kicked off have completed. + if (tasks.Count > 0) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + return dto; + } + + /// + /// Converts a BaseItem to a DTOBaseItem + /// + /// The item. + /// The user. + /// The fields. + /// Task{DtoBaseItem}. + /// + public async static Task GetDtoBaseItem(BaseItem item, User user, List fields) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (fields == null) + { + throw new ArgumentNullException("fields"); + } + + var dto = new DtoBaseItem(); + + var tasks = new List(); + + if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) + { + try + { + tasks.Add(AttachPrimaryImageAspectRatio(dto, item)); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name); + } + } + + if (fields.Contains(ItemFields.Studios)) + { + dto.Studios = item.Studios; + } + + if (fields.Contains(ItemFields.People)) + { + tasks.Add(AttachPeople(dto, item)); + } + + AttachBasicFields(dto, item, fields); + + AttachUserSpecificInfo(dto, item, user, fields); + + // Make sure all the tasks we kicked off have completed. + if (tasks.Count > 0) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + return dto; + } + + /// + /// Attaches the user specific info. + /// + /// The dto. + /// The item. + /// The user. + /// The fields. + private static void AttachUserSpecificInfo(DtoBaseItem dto, BaseItem item, User user, List fields) + { + dto.IsNew = item.IsRecentlyAdded(user); + + if (fields.Contains(ItemFields.UserData)) + { + var userData = item.GetUserData(user, false); + + if (userData != null) + { + dto.UserData = GetDtoUserItemData(userData); + } + } + + if (item.IsFolder && fields.Contains(ItemFields.DisplayPreferences)) + { + dto.DisplayPreferences = ((Folder)item).GetDisplayPrefs(user, false) ?? new DisplayPreferences { UserId = user.Id }; + } + + if (item.IsFolder) + { + if (fields.Contains(ItemFields.ItemCounts)) + { + var folder = (Folder)item; + + // Skip sorting since all we want is a count + dto.ChildCount = folder.GetChildren(user).Count(); + + SetSpecialCounts(folder, user, dto); + } + } + } + + /// + /// Attaches the primary image aspect ratio. + /// + /// The dto. + /// The item. + /// Task. + private static async Task AttachPrimaryImageAspectRatio(DtoBaseItem dto, BaseItem item) + { + var path = item.PrimaryImagePath; + + if (string.IsNullOrEmpty(path)) + { + return; + } + + var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(path); + + // See if we can avoid a file system lookup by looking for the file in ResolveArgs + var dateModified = metaFileEntry == null ? File.GetLastWriteTimeUtc(path) : metaFileEntry.Value.LastWriteTimeUtc; + + ImageSize size; + + try + { + size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + Logger.LogError("Image file does not exist: {0}", path); + return; + } + + foreach (var enhancer in Kernel.Instance.ImageEnhancers + .Where(i => i.Supports(item, ImageType.Primary))) + { + + size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size); + } + + dto.PrimaryImageAspectRatio = size.Width / size.Height; + } + + /// + /// Sets simple property values on a DTOBaseItem + /// + /// The dto. + /// The item. + /// The fields. + private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, List fields) + { + if (fields.Contains(ItemFields.DateCreated)) + { + dto.DateCreated = item.DateCreated; + } + + if (fields.Contains(ItemFields.DisplayMediaType)) + { + dto.DisplayMediaType = item.DisplayMediaType; + } + + dto.AspectRatio = item.AspectRatio; + + dto.BackdropImageTags = GetBackdropImageTags(item); + + if (fields.Contains(ItemFields.Genres)) + { + dto.Genres = item.Genres; + } + + if (item.Images != null) + { + dto.ImageTags = new Dictionary(); + + foreach (var image in item.Images) + { + ImageType type; + + if (Enum.TryParse(image.Key, true, out type)) + { + dto.ImageTags[type] = Kernel.Instance.ImageManager.GetImageCacheTag(item, type, image.Value); + } + } + } + + dto.Id = GetClientItemId(item); + dto.IndexNumber = item.IndexNumber; + dto.IsFolder = item.IsFolder; + dto.Language = item.Language; + dto.MediaType = item.MediaType; + dto.LocationType = item.LocationType; + + var localTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count; + + if (localTrailerCount > 0) + { + dto.LocalTrailerCount = localTrailerCount; + } + + dto.Name = item.Name; + dto.OfficialRating = item.OfficialRating; + + if (fields.Contains(ItemFields.Overview)) + { + dto.Overview = item.Overview; + } + + // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance + if (dto.BackdropImageTags.Count == 0) + { + var parentWithBackdrop = GetParentBackdropItem(item); + + if (parentWithBackdrop != null) + { + dto.ParentBackdropItemId = GetClientItemId(parentWithBackdrop); + dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop); + } + } + + if (item.Parent != null && fields.Contains(ItemFields.ParentId)) + { + dto.ParentId = GetClientItemId(item.Parent); + } + + dto.ParentIndexNumber = item.ParentIndexNumber; + + // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance + if (!dto.HasLogo) + { + var parentWithLogo = GetParentLogoItem(item); + + if (parentWithLogo != null) + { + dto.ParentLogoItemId = GetClientItemId(parentWithLogo); + + dto.ParentLogoImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo)); + } + } + + if (fields.Contains(ItemFields.Path)) + { + dto.Path = item.Path; + } + + dto.PremiereDate = item.PremiereDate; + dto.ProductionYear = item.ProductionYear; + + if (fields.Contains(ItemFields.ProviderIds)) + { + dto.ProviderIds = item.ProviderIds; + } + + dto.RunTimeTicks = item.RunTimeTicks; + + if (fields.Contains(ItemFields.SortName)) + { + dto.SortName = item.SortName; + } + + if (fields.Contains(ItemFields.Taglines)) + { + dto.Taglines = item.Taglines; + } + + if (fields.Contains(ItemFields.TrailerUrls)) + { + dto.TrailerUrls = item.TrailerUrls; + } + + dto.Type = item.GetType().Name; + dto.CommunityRating = item.CommunityRating; + + if (item.IsFolder) + { + var folder = (Folder)item; + + dto.IsRoot = folder.IsRoot; + dto.IsVirtualFolder = folder.IsVirtualFolder; + + if (fields.Contains(ItemFields.IndexOptions)) + { + dto.IndexOptions = folder.IndexByOptionStrings.ToArray(); + } + + if (fields.Contains(ItemFields.SortOptions)) + { + dto.SortOptions = folder.SortByOptionStrings.ToArray(); + } + } + + // Add audio info + var audio = item as Audio; + if (audio != null) + { + if (fields.Contains(ItemFields.AudioInfo)) + { + dto.Album = audio.Album; + dto.AlbumArtist = audio.AlbumArtist; + dto.Artist = audio.Artist; + } + } + + // Add video info + var video = item as Video; + if (video != null) + { + dto.VideoType = video.VideoType; + dto.VideoFormat = video.VideoFormat; + dto.IsoType = video.IsoType; + + if (fields.Contains(ItemFields.Chapters) && video.Chapters != null) + { + dto.Chapters = video.Chapters.Select(c => GetChapterInfoDto(c, item)).ToList(); + } + } + + if (fields.Contains(ItemFields.MediaStreams)) + { + // Add VideoInfo + var iHasMediaStreams = item as IHasMediaStreams; + + if (iHasMediaStreams != null) + { + dto.MediaStreams = iHasMediaStreams.MediaStreams; + } + } + + // Add MovieInfo + var movie = item as Movie; + + if (movie != null) + { + var specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count; + + if (specialFeatureCount > 0) + { + dto.SpecialFeatureCount = specialFeatureCount; + } + } + + if (fields.Contains(ItemFields.SeriesInfo)) + { + // Add SeriesInfo + var series = item as Series; + + if (series != null) + { + dto.AirDays = series.AirDays; + dto.AirTime = series.AirTime; + dto.Status = series.Status; + } + + // Add EpisodeInfo + var episode = item as Episode; + + if (episode != null) + { + series = item.FindParent(); + + dto.SeriesId = GetClientItemId(series); + dto.SeriesName = series.Name; + } + + // Add SeasonInfo + var season = item as Season; + + if (season != null) + { + series = item.FindParent(); + + dto.SeriesId = GetClientItemId(series); + dto.SeriesName = series.Name; + } + } + } + + /// + /// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once + /// + /// The folder. + /// The user. + /// The dto. + private static void SetSpecialCounts(Folder folder, User user, DtoBaseItem dto) + { + var utcNow = DateTime.UtcNow; + + var rcentlyAddedItemCount = 0; + var recursiveItemCount = 0; + var favoriteItemsCount = 0; + var recentlyAddedUnPlayedItemCount = 0; + var resumableItemCount = 0; + var recentlyPlayedItemCount = 0; + + double totalPercentPlayed = 0; + + // Loop through each recursive child + foreach (var child in folder.GetRecursiveChildren(user)) + { + var userdata = child.GetUserData(user, false); + + if (!child.IsFolder) + { + recursiveItemCount++; + + // Check is recently added + if (child.IsRecentlyAdded(user)) + { + rcentlyAddedItemCount++; + + // Check recently added unplayed + if (userdata == null || userdata.PlayCount == 0) + { + recentlyAddedUnPlayedItemCount++; + } + } + + // Incrememt totalPercentPlayed + if (userdata != null) + { + if (userdata.PlayCount > 0) + { + totalPercentPlayed += 100; + } + else if (userdata.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0) + { + double itemPercent = userdata.PlaybackPositionTicks; + itemPercent /= child.RunTimeTicks.Value; + totalPercentPlayed += itemPercent; + } + } + } + + if (userdata != null) + { + if (userdata.IsFavorite) + { + favoriteItemsCount++; + } + + if (userdata.PlaybackPositionTicks > 0) + { + resumableItemCount++; + } + + if (userdata.LastPlayedDate.HasValue && (utcNow - userdata.LastPlayedDate.Value).TotalDays < Kernel.Instance.Configuration.RecentlyPlayedDays) + { + recentlyPlayedItemCount++; + } + } + } + + dto.RecursiveItemCount = recursiveItemCount; + dto.RecentlyAddedItemCount = rcentlyAddedItemCount; + dto.RecentlyAddedUnPlayedItemCount = recentlyAddedUnPlayedItemCount; + dto.ResumableItemCount = resumableItemCount; + dto.FavoriteItemCount = favoriteItemsCount; + dto.RecentlyPlayedItemCount = recentlyPlayedItemCount; + + if (recursiveItemCount > 0) + { + dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount; + } + } + + /// + /// Attaches People DTO's to a DTOBaseItem + /// + /// The dto. + /// The item. + /// Task. + private static async Task AttachPeople(DtoBaseItem dto, BaseItem item) + { + if (item.People == null) + { + return; + } + + // Attach People by transforming them into BaseItemPerson (DTO) + dto.People = new BaseItemPerson[item.People.Count]; + + var entities = await Task.WhenAll(item.People.Select(c => + + Task.Run(async () => + { + try + { + return await Kernel.Instance.LibraryManager.GetPerson(c.Name).ConfigureAwait(false); + } + catch (IOException ex) + { + Logger.LogException("Error getting person {0}", ex, c.Name); + return null; + } + }) + + )).ConfigureAwait(false); + + for (var i = 0; i < item.People.Count; i++) + { + var person = item.People[i]; + + var baseItemPerson = new BaseItemPerson + { + Name = person.Name, + Role = person.Role, + Type = person.Type + }; + + var ibnObject = entities[i]; + + if (ibnObject != null) + { + var primaryImagePath = ibnObject.PrimaryImagePath; + + if (!string.IsNullOrEmpty(primaryImagePath)) + { + baseItemPerson.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(ibnObject, ImageType.Primary, primaryImagePath); + } + } + + dto.People[i] = baseItemPerson; + } + } + + /// + /// If an item does not any backdrops, this can be used to find the first parent that does have one + /// + /// The item. + /// BaseItem. + private static BaseItem GetParentBackdropItem(BaseItem item) + { + var parent = item.Parent; + + while (parent != null) + { + if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Count > 0) + { + return parent; + } + + parent = parent.Parent; + } + + return null; + } + + /// + /// If an item does not have a logo, this can be used to find the first parent that does have one + /// + /// The item. + /// BaseItem. + private static BaseItem GetParentLogoItem(BaseItem item) + { + var parent = item.Parent; + + while (parent != null) + { + if (parent.HasImage(ImageType.Logo)) + { + return parent; + } + + parent = parent.Parent; + } + + return null; + } + + /// + /// Gets the library update info. + /// + /// The instance containing the event data. + /// LibraryUpdateInfo. + internal static LibraryUpdateInfo GetLibraryUpdateInfo(ChildrenChangedEventArgs changeEvent) + { + return new LibraryUpdateInfo + { + Folder = GetBaseItemInfo(changeEvent.Folder), + ItemsAdded = changeEvent.ItemsAdded.Select(GetBaseItemInfo), + ItemsRemoved = changeEvent.ItemsRemoved.Select(i => i.Id), + ItemsUpdated = changeEvent.ItemsUpdated.Select(i => i.Id) + }; + } + + /// + /// Converts a UserItemData to a DTOUserItemData + /// + /// The data. + /// DtoUserItemData. + /// + public static DtoUserItemData GetDtoUserItemData(UserItemData data) + { + if (data == null) + { + throw new ArgumentNullException(); + } + + return new DtoUserItemData + { + IsFavorite = data.IsFavorite, + Likes = data.Likes, + PlaybackPositionTicks = data.PlaybackPositionTicks, + PlayCount = data.PlayCount, + Rating = data.Rating, + Played = data.Played + }; + } + + /// + /// Gets the chapter info dto. + /// + /// The chapter info. + /// The item. + /// ChapterInfoDto. + private static ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item) + { + var dto = new ChapterInfoDto + { + Name = chapterInfo.Name, + StartPositionTicks = chapterInfo.StartPositionTicks + }; + + if (!string.IsNullOrEmpty(chapterInfo.ImagePath)) + { + dto.ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.ChapterImage, chapterInfo.ImagePath); + } + + return dto; + } + + /// + /// Converts a BaseItem to a BaseItemInfo + /// + /// The item. + /// BaseItemInfo. + /// item + public static BaseItemInfo GetBaseItemInfo(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var info = new BaseItemInfo + { + Id = GetClientItemId(item), + Name = item.Name, + Type = item.GetType().Name, + IsFolder = item.IsFolder, + RunTimeTicks = item.RunTimeTicks + }; + + var imagePath = item.PrimaryImagePath; + + if (!string.IsNullOrEmpty(imagePath)) + { + info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath); + } + + if (item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0) + { + imagePath = item.BackdropImagePaths[0]; + + if (!string.IsNullOrEmpty(imagePath)) + { + info.BackdropImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, imagePath); + } + } + + return info; + } + + /// + /// Gets client-side Id of a server-side BaseItem + /// + /// The item. + /// System.String. + /// item + public static string GetClientItemId(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var indexFolder = item as IndexFolder; + + if (indexFolder != null) + { + return GetClientItemId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id; + } + + return item.Id.ToString(); + } + + /// + /// Converts a User to a DTOUser + /// + /// The user. + /// DtoUser. + /// user + public static DtoUser GetDtoUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var dto = new DtoUser + { + Id = user.Id, + Name = user.Name, + HasPassword = !String.IsNullOrEmpty(user.Password), + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration + }; + + var image = user.PrimaryImagePath; + + if (!string.IsNullOrEmpty(image)) + { + dto.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(user, ImageType.Primary, image); + } + + return dto; + } + + /// + /// Gets a BaseItem based upon it's client-side item id + /// + /// The id. + /// The user id. + /// BaseItem. + public static BaseItem GetItemByClientId(string id, Guid? userId = null) + { + var isIdEmpty = string.IsNullOrEmpty(id); + + // If the item is an indexed folder we have to do a special routine to get it + var isIndexFolder = !isIdEmpty && + id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1; + + if (isIndexFolder) + { + if (userId.HasValue) + { + return GetIndexFolder(id, userId.Value); + } + } + + BaseItem item = null; + + if (userId.HasValue) + { + item = isIdEmpty + ? Kernel.Instance.GetUserById(userId.Value).RootFolder + : Kernel.Instance.GetItemById(new Guid(id), userId.Value); + } + else if (!isIndexFolder) + { + item = Kernel.Instance.GetItemById(new Guid(id)); + } + + // If we still don't find it, look within individual user views + if (item == null && !userId.HasValue) + { + foreach (var user in Kernel.Instance.Users) + { + item = GetItemByClientId(id, user.Id); + + if (item != null) + { + break; + } + } + } + + return item; + } + + /// + /// Finds an index folder based on an Id and userId + /// + /// The id. + /// The user id. + /// BaseItem. + private static BaseItem GetIndexFolder(string id, Guid userId) + { + var user = Kernel.Instance.GetUserById(userId); + + var stringSeparators = new[] { IndexFolderDelimeter }; + + // Split using the delimeter + var values = id.Split(stringSeparators, StringSplitOptions.None).ToList(); + + // Get the top folder normally using the first id + var folder = GetItemByClientId(values[0], userId) as Folder; + + values.RemoveAt(0); + + // Get indexed folders using the remaining values in the id string + return GetIndexFolder(values, folder, user); + } + + /// + /// Gets indexed folders based on a list of index names and folder id's + /// + /// The values. + /// The parent folder. + /// The user. + /// BaseItem. + private static BaseItem GetIndexFolder(List values, Folder parentFolder, User user) + { + // The index name is first + var indexBy = values[0]; + + // The index folder id is next + var indexFolderId = new Guid(values[1]); + + // Remove them from the lst + values.RemoveRange(0, 2); + + // Get the IndexFolder + var indexFolder = parentFolder.GetChildren(user, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder; + + // Nested index folder + if (values.Count > 0) + { + return GetIndexFolder(values, indexFolder, user); + } + + return indexFolder; + } + + /// + /// Gets the backdrop image tags. + /// + /// The item. + /// List{System.String}. + private static List GetBackdropImageTags(BaseItem item) + { + if (item.BackdropImagePaths == null) + { + return new List(); + } + + return item.BackdropImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, p)).ToList(); + } + } +} diff --git a/MediaBrowser.Controller/Library/ItemController.cs b/MediaBrowser.Controller/Library/ItemController.cs deleted file mode 100644 index 54673e538d..0000000000 --- a/MediaBrowser.Controller/Library/ItemController.cs +++ /dev/null @@ -1,136 +0,0 @@ -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; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Library -{ - public class ItemController - { - - /// - /// Resolves a path into a BaseItem - /// - public async Task GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null, bool allowInternetProviders = true) - { - var args = new ItemResolveEventArgs - { - FileInfo = fileInfo ?? FileData.GetFileData(path), - Parent = parent, - Cancel = false, - Path = path - }; - - // Gather child folder and files - if (args.IsDirectory) - { - args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray(); - - bool isVirtualFolder = parent != null && parent.IsRoot; - args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder); - } - else - { - args.FileSystemChildren = new WIN32_FIND_DATA[] { }; - } - - - // Check to see if we should resolve based on our contents - if (!EntityResolutionHelper.ShouldResolvePathContents(args)) - { - return null; - } - - BaseItem item = Kernel.Instance.ResolveItem(args); - - return item; - } - - /// - /// Gets a Person - /// - public Task GetPerson(string name) - { - return GetImagesByNameItem(Kernel.Instance.ApplicationPaths.PeoplePath, name); - } - - /// - /// Gets a Studio - /// - public Task GetStudio(string name) - { - return GetImagesByNameItem(Kernel.Instance.ApplicationPaths.StudioPath, name); - } - - /// - /// Gets a Genre - /// - public Task GetGenre(string name) - { - return GetImagesByNameItem(Kernel.Instance.ApplicationPaths.GenrePath, name); - } - - /// - /// Gets a Year - /// - public Task GetYear(int value) - { - return GetImagesByNameItem(Kernel.Instance.ApplicationPaths.YearPath, value.ToString()); - } - - private readonly ConcurrentDictionary ImagesByNameItemCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Generically retrieves an IBN item - /// - private Task GetImagesByNameItem(string path, string name) - where T : BaseEntity, new() - { - name = FileData.GetValidFilename(name); - - path = Path.Combine(path, name); - - // Look for it in the cache, if it's not there, create it - if (!ImagesByNameItemCache.ContainsKey(path)) - { - ImagesByNameItemCache[path] = CreateImagesByNameItem(path, name); - } - - return ImagesByNameItemCache[path] as Task; - } - - /// - /// Creates an IBN item based on a given path - /// - private async Task CreateImagesByNameItem(string path, string name) - where T : BaseEntity, new() - { - var item = new T { }; - - item.Name = name; - item.Id = path.GetMD5(); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - item.DateCreated = Directory.GetCreationTimeUtc(path); - item.DateModified = Directory.GetLastWriteTimeUtc(path); - - var args = new ItemResolveEventArgs { }; - args.FileInfo = FileData.GetFileData(path); - args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray(); - - await Kernel.Instance.ExecuteMetadataProviders(item).ConfigureAwait(false); - - return item; - } - } -} diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs new file mode 100644 index 0000000000..c95300f745 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -0,0 +1,397 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Win32; +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Controller.Library +{ + /// + /// These are arguments relating to the file system that are collected once and then referred to + /// whenever needed. Primarily for entity resolution. + /// + public class ItemResolveArgs : EventArgs + { + /// + /// Gets the file system children. + /// + /// The file system children. + public IEnumerable FileSystemChildren + { + get { return FileSystemDictionary.Values; } + } + + /// + /// Gets or sets the file system dictionary. + /// + /// The file system dictionary. + public Dictionary FileSystemDictionary { get; set; } + + /// + /// Gets or sets the parent. + /// + /// The parent. + public Folder Parent { get; set; } + + /// + /// Gets or sets the file info. + /// + /// The file info. + public WIN32_FIND_DATA FileInfo { get; set; } + + /// + /// Gets or sets the path. + /// + /// The path. + public string Path { get; set; } + + /// + /// Gets a value indicating whether this instance is directory. + /// + /// true if this instance is directory; otherwise, false. + public bool IsDirectory + { + get + { + return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory); + } + } + + /// + /// Gets a value indicating whether this instance is hidden. + /// + /// true if this instance is hidden; otherwise, false. + public bool IsHidden + { + get + { + return FileInfo.IsHidden; + } + } + + /// + /// Gets a value indicating whether this instance is system file. + /// + /// true if this instance is system file; otherwise, false. + public bool IsSystemFile + { + get + { + return FileInfo.IsSystemFile; + } + } + + /// + /// Gets a value indicating whether this instance is vf. + /// + /// true if this instance is vf; otherwise, false. + public bool IsVf + { + // we should be considered a virtual folder if we are a child of one of the children of the system root folder. + // this is a bit of a trick to determine that... the directory name of a sub-child of the root will start with + // the root but not be equal to it + get + { + if (!IsDirectory) + { + return false; + } + + var parentDir = FileInfo.Path != null ? System.IO.Path.GetDirectoryName(FileInfo.Path) ?? string.Empty : string.Empty; + + return (parentDir.Length > Kernel.Instance.ApplicationPaths.RootFolderPath.Length + && parentDir.StartsWith(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase)); + + } + } + + /// + /// Gets a value indicating whether this instance is physical root. + /// + /// true if this instance is physical root; otherwise, false. + public bool IsPhysicalRoot + { + get + { + return IsDirectory && Path.Equals(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase); + } + } + + /// + /// Gets a value indicating whether this instance is root. + /// + /// true if this instance is root; otherwise, false. + public bool IsRoot + { + get + { + return Parent == null; + } + } + + /// + /// Gets or sets the additional locations. + /// + /// The additional locations. + private List AdditionalLocations { get; set; } + + /// + /// Adds the additional location. + /// + /// The path. + /// + public void AddAdditionalLocation(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + if (AdditionalLocations == null) + { + AdditionalLocations = new List(); + } + + AdditionalLocations.Add(path); + } + + /// + /// Gets the physical locations. + /// + /// The physical locations. + public IEnumerable PhysicalLocations + { + get + { + var paths = string.IsNullOrWhiteSpace(Path) ? new string[] {} : new[] {Path}; + return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations); + } + } + + /// + /// Store these to reduce disk access in Resolvers + /// + /// The metadata file dictionary. + private Dictionary MetadataFileDictionary { get; set; } + + /// + /// Gets the metadata files. + /// + /// The metadata files. + public IEnumerable MetadataFiles + { + get + { + if (MetadataFileDictionary != null) + { + return MetadataFileDictionary.Values; + } + + return new WIN32_FIND_DATA[] {}; + } + } + + /// + /// Adds the metadata file. + /// + /// The path. + /// + public void AddMetadataFile(string path) + { + var file = FileSystem.GetFileData(path); + + if (!file.HasValue) + { + throw new FileNotFoundException(path); + } + + AddMetadataFile(file.Value); + } + + /// + /// Adds the metadata file. + /// + /// The file info. + public void AddMetadataFile(WIN32_FIND_DATA fileInfo) + { + AddMetadataFiles(new[] { fileInfo }); + } + + /// + /// Adds the metadata files. + /// + /// The files. + /// + public void AddMetadataFiles(IEnumerable files) + { + if (files == null) + { + throw new ArgumentNullException(); + } + + if (MetadataFileDictionary == null) + { + MetadataFileDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + foreach (var file in files) + { + MetadataFileDictionary[file.cFileName] = file; + } + } + + /// + /// Gets the name of the file system entry by. + /// + /// The name. + /// System.Nullable{WIN32_FIND_DATA}. + /// + public WIN32_FIND_DATA? GetFileSystemEntryByName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(); + } + + return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name)); + } + + /// + /// Gets the file system entry by path. + /// + /// The path. + /// System.Nullable{WIN32_FIND_DATA}. + /// + public WIN32_FIND_DATA? GetFileSystemEntryByPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + if (FileSystemDictionary != null) + { + WIN32_FIND_DATA entry; + + if (FileSystemDictionary.TryGetValue(path, out entry)) + { + return entry; + } + } + + return null; + } + + /// + /// Gets the meta file by path. + /// + /// The path. + /// System.Nullable{WIN32_FIND_DATA}. + /// + public WIN32_FIND_DATA? GetMetaFileByPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + if (MetadataFileDictionary != null) + { + WIN32_FIND_DATA entry; + + if (MetadataFileDictionary.TryGetValue(System.IO.Path.GetFileName(path), out entry)) + { + return entry; + } + } + + return GetFileSystemEntryByPath(path); + } + + /// + /// Gets the name of the meta file by. + /// + /// The name. + /// System.Nullable{WIN32_FIND_DATA}. + /// + public WIN32_FIND_DATA? GetMetaFileByName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(); + } + + if (MetadataFileDictionary != null) + { + WIN32_FIND_DATA entry; + + if (MetadataFileDictionary.TryGetValue(name, out entry)) + { + return entry; + } + } + + return GetFileSystemEntryByName(name); + } + + /// + /// Determines whether [contains meta file by name] [the specified name]. + /// + /// The name. + /// true if [contains meta file by name] [the specified name]; otherwise, false. + public bool ContainsMetaFileByName(string name) + { + return GetMetaFileByName(name).HasValue; + } + + /// + /// Determines whether [contains file system entry by name] [the specified name]. + /// + /// The name. + /// true if [contains file system entry by name] [the specified name]; otherwise, false. + public bool ContainsFileSystemEntryByName(string name) + { + return GetFileSystemEntryByName(name).HasValue; + } + + #region Equality Overrides + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The object to compare with the current object. + /// true if the specified is equal to this instance; otherwise, false. + public override bool Equals(object obj) + { + return (Equals(obj as ItemResolveArgs)); + } + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + return Path.GetHashCode(); + } + + /// + /// Equalses the specified args. + /// + /// The args. + /// true if XXXX, false otherwise + protected bool Equals(ItemResolveArgs args) + { + if (args != null) + { + if (args.Path == null && Path == null) return true; + return args.Path != null && args.Path.Equals(Path, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + #endregion + } + +} diff --git a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs deleted file mode 100644 index 32b8783dfd..0000000000 --- a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs +++ /dev/null @@ -1,104 +0,0 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.IO; -using System.Collections.Generic; -using System.Linq; -using System; -using System.IO; - -namespace MediaBrowser.Controller.Library -{ - /// - /// This is an EventArgs object used when resolving a Path into a BaseItem - /// - public class ItemResolveEventArgs : PreBeginResolveEventArgs - { - public WIN32_FIND_DATA[] FileSystemChildren { get; set; } - - protected List _additionalLocations = new List(); - public List AdditionalLocations - { - get - { - return _additionalLocations; - } - set - { - _additionalLocations = value; - } - } - - public IEnumerable PhysicalLocations - { - get - { - return (new List() {this.Path}).Concat(AdditionalLocations); - } - } - - public bool IsBDFolder { get; set; } - public bool IsDVDFolder { get; set; } - public bool IsHDDVDFolder { get; set; } - - /// - /// Store these to reduce disk access in Resolvers - /// - public string[] MetadataFiles { 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) - { - return ContainsFile(name); - } - } - - /// - /// This is an EventArgs object used before we begin resolving a Path into a BaseItem - /// File system children have not been collected yet, but consuming events will - /// have a chance to cancel resolution based on the Path, Parent and FileAttributes - /// - public class PreBeginResolveEventArgs : EventArgs - { - public Folder Parent { get; set; } - - public bool Cancel { get; set; } - - public WIN32_FIND_DATA FileInfo { get; set; } - - public string Path { get; set; } - - public bool IsDirectory - { - get - { - return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory); - } - } - - public bool IsHidden - { - get - { - return FileInfo.IsHidden; - } - } - - public bool IsSystemFile - { - get - { - return FileInfo.IsSystemFile; - } - } - - } -} diff --git a/MediaBrowser.Controller/Library/LibraryManager.cs b/MediaBrowser.Controller/Library/LibraryManager.cs new file mode 100644 index 0000000000..7d3c764b2d --- /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() + }); + } + } +} diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs new file mode 100644 index 0000000000..4daa9d654c --- /dev/null +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using MediaBrowser.Common.Logging; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Class Profiler + /// + public class Profiler : IDisposable + { + /// + /// The name + /// + readonly string name; + /// + /// The stopwatch + /// + readonly Stopwatch stopwatch; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + public Profiler(string name) + { + this.name = name; + + stopwatch = new Stopwatch(); + stopwatch.Start(); + } + #region IDisposable Members + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + stopwatch.Stop(); + string message; + if (stopwatch.ElapsedMilliseconds > 300000) + { + message = string.Format("{0} took {1} minutes.", + name, ((float)stopwatch.ElapsedMilliseconds / 60000).ToString("F")); + } + else + { + message = string.Format("{0} took {1} seconds.", + name, ((float)stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000")); + } + Logger.LogInfo(message); + } + } + + #endregion + } +} diff --git a/MediaBrowser.Controller/Library/ResourcePool.cs b/MediaBrowser.Controller/Library/ResourcePool.cs new file mode 100644 index 0000000000..3e4d532041 --- /dev/null +++ b/MediaBrowser.Controller/Library/ResourcePool.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading; + +namespace MediaBrowser.Controller.Library +{ + /// + /// This is just a collection of semaphores to control the number of concurrent executions of various resources + /// + public class ResourcePool : IDisposable + { + /// + /// You tube + /// + public readonly SemaphoreSlim YouTube = new SemaphoreSlim(5, 5); + + /// + /// The trakt + /// + public readonly SemaphoreSlim Trakt = new SemaphoreSlim(5, 5); + + /// + /// The tv db + /// + public readonly SemaphoreSlim TvDb = new SemaphoreSlim(5, 5); + + /// + /// The movie db + /// + public readonly SemaphoreSlim MovieDb = new SemaphoreSlim(5, 5); + + /// + /// The fan art + /// + public readonly SemaphoreSlim FanArt = new SemaphoreSlim(5, 5); + + /// + /// The mb + /// + public readonly SemaphoreSlim Mb = new SemaphoreSlim(5, 5); + + /// + /// Apple doesn't seem to like too many simulataneous requests. + /// + public readonly SemaphoreSlim AppleTrailerVideos = new SemaphoreSlim(1, 1); + + /// + /// The apple trailer images + /// + public readonly SemaphoreSlim AppleTrailerImages = new SemaphoreSlim(1, 1); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + YouTube.Dispose(); + Trakt.Dispose(); + TvDb.Dispose(); + MovieDb.Dispose(); + FanArt.Dispose(); + Mb.Dispose(); + AppleTrailerVideos.Dispose(); + AppleTrailerImages.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Controller/Library/UserDataManager.cs b/MediaBrowser.Controller/Library/UserDataManager.cs new file mode 100644 index 0000000000..dfa80483ec --- /dev/null +++ b/MediaBrowser.Controller/Library/UserDataManager.cs @@ -0,0 +1,219 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Connectivity; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Class UserDataManager + /// + public class UserDataManager : BaseManager + { + #region Events + /// + /// Occurs when [playback start]. + /// + public event EventHandler PlaybackStart; + /// + /// Occurs when [playback progress]. + /// + public event EventHandler PlaybackProgress; + /// + /// Occurs when [playback stopped]. + /// + public event EventHandler PlaybackStopped; + #endregion + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public UserDataManager(Kernel kernel) + : base(kernel) + { + + } + + /// + /// Used to report that playback has started for an item + /// + /// The user. + /// The item. + /// Type of the client. + /// Name of the device. + /// + public void OnPlaybackStart(User user, BaseItem item, ClientType clientType, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item); + + // Nothing to save here + // Fire events to inform plugins + EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs + { + Argument = item, + User = user + }); + } + + /// + /// Used to report playback progress for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// Name of the device. + /// Task. + /// + public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item, positionTicks); + + if (positionTicks.HasValue) + { + var data = item.GetUserData(user, true); + + UpdatePlayState(item, data, positionTicks.Value, false); + await SaveUserDataForItem(user, item, data).ConfigureAwait(false); + } + + EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs + { + Argument = item, + User = user, + PlaybackPositionTicks = positionTicks + }); + } + + /// + /// Used to report that playback has ended for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// Name of the device. + /// Task. + /// + public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + Kernel.UserManager.RemoveNowPlayingItemId(user, clientType, deviceName, item); + + var data = item.GetUserData(user, true); + + if (positionTicks.HasValue) + { + UpdatePlayState(item, data, positionTicks.Value, true); + } + else + { + // If the client isn't able to report this, then we'll just have to make an assumption + data.PlayCount++; + data.Played = true; + } + + await SaveUserDataForItem(user, item, data).ConfigureAwait(false); + + EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs + { + Argument = item, + User = user, + PlaybackPositionTicks = positionTicks + }); + } + + /// + /// Updates playstate position for an item but does not save + /// + /// The item + /// User data for the item + /// The current playback position + /// Whether or not to increment playcount + private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount) + { + // If a position has been reported, and if we know the duration + if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0) + { + var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; + + // Don't track in very beginning + if (pctIn < Kernel.Configuration.MinResumePct) + { + positionTicks = 0; + incrementPlayCount = false; + } + + // If we're at the end, assume completed + else if (pctIn > Kernel.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) + { + positionTicks = 0; + data.Played = true; + } + + else + { + // Enforce MinResumeDuration + var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; + + if (durationSeconds < Kernel.Configuration.MinResumeDurationSeconds) + { + positionTicks = 0; + data.Played = true; + } + } + } + + data.PlaybackPositionTicks = positionTicks; + + if (incrementPlayCount) + { + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + } + } + + /// + /// Saves user data for an item + /// + /// The user. + /// The item. + /// The data. + public Task SaveUserDataForItem(User user, BaseItem item, UserItemData data) + { + item.AddOrUpdateUserData(user, data); + + return Kernel.UserDataRepository.SaveUserData(item, CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Controller/Library/UserManager.cs b/MediaBrowser.Controller/Library/UserManager.cs new file mode 100644 index 0000000000..af3239657b --- /dev/null +++ b/MediaBrowser.Controller/Library/UserManager.cs @@ -0,0 +1,395 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Connectivity; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Class UserManager + /// + public class UserManager : BaseManager + { + /// + /// The _active connections + /// + private readonly ConcurrentBag _activeConnections = + new ConcurrentBag(); + + /// + /// Gets all connections. + /// + /// All connections. + public IEnumerable AllConnections + { + get { return _activeConnections.Where(c => Kernel.GetUserById(c.UserId) != null).OrderByDescending(c => c.LastActivityDate); } + } + + /// + /// Gets the active connections. + /// + /// The active connections. + public IEnumerable ActiveConnections + { + get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 10); } + } + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public UserManager(Kernel kernel) + : base(kernel) + { + } + + #region UserUpdated Event + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + + /// + /// Called when [user updated]. + /// + /// The user. + internal void OnUserUpdated(User user) + { + EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs { Argument = user }); + + // Notify connected ui's + Kernel.TcpManager.SendWebSocketMessage("UserUpdated", DtoBuilder.GetDtoUser(user)); + } + #endregion + + #region UserDeleted Event + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + /// + /// Called when [user deleted]. + /// + /// The user. + internal void OnUserDeleted(User user) + { + EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs { Argument = user }); + + // Notify connected ui's + Kernel.TcpManager.SendWebSocketMessage("UserDeleted", user.Id.ToString()); + } + #endregion + + /// + /// Authenticates a User and returns a result indicating whether or not it succeeded + /// + /// The user. + /// The password. + /// Task{System.Boolean}. + /// user + public async Task AuthenticateUser(User user, string password) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + password = password ?? string.Empty; + var existingPassword = string.IsNullOrEmpty(user.Password) ? string.Empty.GetMD5().ToString() : user.Password; + + var success = password.GetMD5().ToString().Equals(existingPassword); + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + await UpdateUser(user).ConfigureAwait(false); + } + + Logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied")); + + return success; + } + + /// + /// Logs the user activity. + /// + /// The user. + /// Type of the client. + /// Name of the device. + /// Task. + /// user + public Task LogUserActivity(User user, ClientType clientType, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var activityDate = DateTime.UtcNow; + + user.LastActivityDate = activityDate; + + LogConnection(user.Id, clientType, deviceName, activityDate); + + // Save this directly. No need to fire off all the events for this. + return Kernel.UserRepository.SaveUser(user, CancellationToken.None); + } + + /// + /// Updates the now playing item id. + /// + /// The user. + /// Type of the client. + /// Name of the device. + /// The item. + /// The current position ticks. + public void UpdateNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item, long? currentPositionTicks = null) + { + var conn = GetConnection(user.Id, clientType, deviceName); + + conn.NowPlayingPositionTicks = currentPositionTicks; + conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item); + } + + /// + /// Removes the now playing item id. + /// + /// The user. + /// Type of the client. + /// Name of the device. + /// The item. + public void RemoveNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item) + { + var conn = GetConnection(user.Id, clientType, deviceName); + + if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString())) + { + conn.NowPlayingItem = null; + conn.NowPlayingPositionTicks = null; + } + } + + /// + /// Logs the connection. + /// + /// The user id. + /// Type of the client. + /// Name of the device. + /// The last activity date. + private void LogConnection(Guid userId, ClientType clientType, string deviceName, DateTime lastActivityDate) + { + GetConnection(userId, clientType, deviceName).LastActivityDate = lastActivityDate; + } + + /// + /// Gets the connection. + /// + /// The user id. + /// Type of the client. + /// Name of the device. + /// ClientConnectionInfo. + private ClientConnectionInfo GetConnection(Guid userId, ClientType clientType, string deviceName) + { + var conn = _activeConnections.FirstOrDefault(c => c.UserId == userId && c.ClientType == clientType && string.Equals(deviceName, c.DeviceName, StringComparison.OrdinalIgnoreCase)); + + if (conn == null) + { + conn = new ClientConnectionInfo + { + UserId = userId, + ClientType = clientType, + DeviceName = deviceName + }; + + _activeConnections.Add(conn); + } + + return conn; + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + internal IEnumerable LoadUsers() + { + var users = Kernel.UserRepository.RetrieveAllUsers().ToList(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var name = Environment.UserName; + + var user = InstantiateNewUser(name); + + var task = Kernel.UserRepository.SaveUser(user, CancellationToken.None); + + // Hate having to block threads + Task.WaitAll(task); + + users.Add(user); + } + + return users; + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// if set to true [force]. + /// Task. + public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false) + { + var tasks = Kernel.Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList(); + + return Task.WhenAll(tasks); + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException("newName"); + } + + if (Kernel.Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + public async Task UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (user.Id == Guid.Empty || !Kernel.Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + + await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + + OnUserUpdated(user); + } + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task CreateUser(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + if (Kernel.Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); + } + + var user = InstantiateNewUser(name); + + var list = Kernel.Users.ToList(); + list.Add(user); + Kernel.Users = list; + + await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + + return user; + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (Kernel.Users.FirstOrDefault(u => u.Id == user.Id) == null) + { + throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + } + + if (Kernel.Users.Count() == 1) + { + throw new ArgumentException(string.Format("The user '{0}' be deleted because there must be at least one user in the system.", user.Name)); + } + + await Kernel.UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); + + OnUserDeleted(user); + + // Force this to be lazy loaded again + Kernel.Users = null; + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + private User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = ("MBUser" + name).GetMD5(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow + }; + } + } +} -- cgit v1.2.3