aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Library
diff options
context:
space:
mode:
authorLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
committerLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
commit767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch)
tree49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/Library
parent845554722efaed872948a9e0f7202e3ef52f1b6e (diff)
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/Library')
-rw-r--r--MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs171
-rw-r--r--MediaBrowser.Controller/Library/DtoBuilder.cs934
-rw-r--r--MediaBrowser.Controller/Library/ItemController.cs136
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs397
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveEventArgs.cs104
-rw-r--r--MediaBrowser.Controller/Library/LibraryManager.cs511
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs69
-rw-r--r--MediaBrowser.Controller/Library/ResourcePool.cs79
-rw-r--r--MediaBrowser.Controller/Library/UserDataManager.cs219
-rw-r--r--MediaBrowser.Controller/Library/UserManager.cs395
10 files changed, 2741 insertions, 274 deletions
diff --git a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
index 462fcc6d6..94f4c540f 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<BaseItem> ItemsAdded { get; set; }
- public IEnumerable<BaseItem> ItemsRemoved { get; set; }
-
- public ChildrenChangedEventArgs()
- {
- //initialize the list
- ItemsAdded = new List<BaseItem>();
- }
-
- /// <summary>
- /// Create the args and set the folder property
- /// </summary>
- /// <param name="folder"></param>
- public ChildrenChangedEventArgs(Folder folder)
- {
- //init the folder property
- this.Folder = folder;
- //init the list
- ItemsAdded = new List<BaseItem>();
- }
- }
-}
+using System.Collections.Concurrent;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class ChildrenChangedEventArgs
+ /// </summary>
+ public class ChildrenChangedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the folder.
+ /// </summary>
+ /// <value>The folder.</value>
+ public Folder Folder { get; set; }
+ /// <summary>
+ /// Gets or sets the items added.
+ /// </summary>
+ /// <value>The items added.</value>
+ public ConcurrentBag<BaseItem> ItemsAdded { get; set; }
+ /// <summary>
+ /// Gets or sets the items removed.
+ /// </summary>
+ /// <value>The items removed.</value>
+ public List<BaseItem> ItemsRemoved { get; set; }
+ /// <summary>
+ /// Gets or sets the items updated.
+ /// </summary>
+ /// <value>The items updated.</value>
+ public ConcurrentBag<BaseItem> ItemsUpdated { get; set; }
+
+ /// <summary>
+ /// Create the args and set the folder property
+ /// </summary>
+ /// <param name="folder">The folder.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public ChildrenChangedEventArgs(Folder folder)
+ {
+ if (folder == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ //init the folder property
+ Folder = folder;
+ //init the list
+ ItemsAdded = new ConcurrentBag<BaseItem>();
+ ItemsRemoved = new List<BaseItem>();
+ ItemsUpdated = new ConcurrentBag<BaseItem>();
+ }
+
+ /// <summary>
+ /// Adds the new item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddNewItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsAdded.Add(item);
+ }
+
+ /// <summary>
+ /// Adds the updated item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddUpdatedItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsUpdated.Add(item);
+ }
+
+ /// <summary>
+ /// Adds the removed item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddRemovedItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ ItemsRemoved.Add(item);
+ }
+
+ /// <summary>
+ /// Lists the has change.
+ /// </summary>
+ /// <param name="list">The list.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool ListHasChange(List<BaseItem> list)
+ {
+ return list != null && list.Count > 0;
+ }
+
+ /// <summary>
+ /// Lists the has change.
+ /// </summary>
+ /// <param name="list">The list.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool ListHasChange(ConcurrentBag<BaseItem> list)
+ {
+ return list != null && !list.IsEmpty;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has change.
+ /// </summary>
+ /// <value><c>true</c> if this instance has change; otherwise, <c>false</c>.</value>
+ public bool HasChange
+ {
+ get { return HasAddOrRemoveChange || ListHasChange(ItemsUpdated); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has add or remove change.
+ /// </summary>
+ /// <value><c>true</c> if this instance has add or remove change; otherwise, <c>false</c>.</value>
+ 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 000000000..b16671e9e
--- /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
+{
+ /// <summary>
+ /// Generates DTO's from domain entities
+ /// </summary>
+ public static class DtoBuilder
+ {
+ /// <summary>
+ /// The index folder delimeter
+ /// </summary>
+ const string IndexFolderDelimeter = "-index-";
+
+ /// <summary>
+ /// Gets the dto base item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="fields">The fields.</param>
+ /// <returns>Task{DtoBaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, List<ItemFields> fields)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+
+ var dto = new DtoBaseItem();
+
+ var tasks = new List<Task>();
+
+ 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;
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a DTOBaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="fields">The fields.</param>
+ /// <returns>Task{DtoBaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user, List<ItemFields> 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<Task>();
+
+ 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;
+ }
+
+ /// <summary>
+ /// Attaches the user specific info.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="fields">The fields.</param>
+ private static void AttachUserSpecificInfo(DtoBaseItem dto, BaseItem item, User user, List<ItemFields> 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);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Attaches the primary image aspect ratio.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Sets simple property values on a DTOBaseItem
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="fields">The fields.</param>
+ private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, List<ItemFields> 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<ImageType, Guid>();
+
+ 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<Series>();
+
+ dto.SeriesId = GetClientItemId(series);
+ dto.SeriesName = series.Name;
+ }
+
+ // Add SeasonInfo
+ var season = item as Season;
+
+ if (season != null)
+ {
+ series = item.FindParent<Series>();
+
+ dto.SeriesId = GetClientItemId(series);
+ dto.SeriesName = series.Name;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once
+ /// </summary>
+ /// <param name="folder">The folder.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="dto">The dto.</param>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Attaches People DTO's to a DTOBaseItem
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>Task.</returns>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// If an item does not any backdrops, this can be used to find the first parent that does have one
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItem.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// If an item does not have a logo, this can be used to find the first parent that does have one
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItem.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the library update info.
+ /// </summary>
+ /// <param name="changeEvent">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ /// <returns>LibraryUpdateInfo.</returns>
+ 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)
+ };
+ }
+
+ /// <summary>
+ /// Converts a UserItemData to a DTOUserItemData
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <returns>DtoUserItemData.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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
+ };
+ }
+
+ /// <summary>
+ /// Gets the chapter info dto.
+ /// </summary>
+ /// <param name="chapterInfo">The chapter info.</param>
+ /// <param name="item">The item.</param>
+ /// <returns>ChapterInfoDto.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a BaseItemInfo
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>BaseItemInfo.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets client-side Id of a server-side BaseItem
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ 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();
+ }
+
+ /// <summary>
+ /// Converts a User to a DTOUser
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>DtoUser.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets a BaseItem based upon it's client-side item id
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>BaseItem.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Finds an index folder based on an Id and userId
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>BaseItem.</returns>
+ 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);
+ }
+
+ /// <summary>
+ /// Gets indexed folders based on a list of index names and folder id's
+ /// </summary>
+ /// <param name="values">The values.</param>
+ /// <param name="parentFolder">The parent folder.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>BaseItem.</returns>
+ private static BaseItem GetIndexFolder(List<string> 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;
+ }
+
+ /// <summary>
+ /// Gets the backdrop image tags.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>List{System.String}.</returns>
+ private static List<Guid> GetBackdropImageTags(BaseItem item)
+ {
+ if (item.BackdropImagePaths == null)
+ {
+ return new List<Guid>();
+ }
+
+ 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 54673e538..000000000
--- 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
- {
-
- /// <summary>
- /// Resolves a path into a BaseItem
- /// </summary>
- public async Task<BaseItem> 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;
- }
-
- /// <summary>
- /// Gets a Person
- /// </summary>
- public Task<Person> GetPerson(string name)
- {
- return GetImagesByNameItem<Person>(Kernel.Instance.ApplicationPaths.PeoplePath, name);
- }
-
- /// <summary>
- /// Gets a Studio
- /// </summary>
- public Task<Studio> GetStudio(string name)
- {
- return GetImagesByNameItem<Studio>(Kernel.Instance.ApplicationPaths.StudioPath, name);
- }
-
- /// <summary>
- /// Gets a Genre
- /// </summary>
- public Task<Genre> GetGenre(string name)
- {
- return GetImagesByNameItem<Genre>(Kernel.Instance.ApplicationPaths.GenrePath, name);
- }
-
- /// <summary>
- /// Gets a Year
- /// </summary>
- public Task<Year> GetYear(int value)
- {
- return GetImagesByNameItem<Year>(Kernel.Instance.ApplicationPaths.YearPath, value.ToString());
- }
-
- private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
-
- /// <summary>
- /// Generically retrieves an IBN item
- /// </summary>
- private Task<T> GetImagesByNameItem<T>(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<T>(path, name);
- }
-
- return ImagesByNameItemCache[path] as Task<T>;
- }
-
- /// <summary>
- /// Creates an IBN item based on a given path
- /// </summary>
- private async Task<T> CreateImagesByNameItem<T>(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 000000000..c95300f74
--- /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
+{
+ /// <summary>
+ /// These are arguments relating to the file system that are collected once and then referred to
+ /// whenever needed. Primarily for entity resolution.
+ /// </summary>
+ public class ItemResolveArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets the file system children.
+ /// </summary>
+ /// <value>The file system children.</value>
+ public IEnumerable<WIN32_FIND_DATA> FileSystemChildren
+ {
+ get { return FileSystemDictionary.Values; }
+ }
+
+ /// <summary>
+ /// Gets or sets the file system dictionary.
+ /// </summary>
+ /// <value>The file system dictionary.</value>
+ public Dictionary<string, WIN32_FIND_DATA> FileSystemDictionary { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent.
+ /// </summary>
+ /// <value>The parent.</value>
+ public Folder Parent { get; set; }
+
+ /// <summary>
+ /// Gets or sets the file info.
+ /// </summary>
+ /// <value>The file info.</value>
+ public WIN32_FIND_DATA FileInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is directory.
+ /// </summary>
+ /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
+ public bool IsDirectory
+ {
+ get
+ {
+ return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get
+ {
+ return FileInfo.IsHidden;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is system file.
+ /// </summary>
+ /// <value><c>true</c> if this instance is system file; otherwise, <c>false</c>.</value>
+ public bool IsSystemFile
+ {
+ get
+ {
+ return FileInfo.IsSystemFile;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is vf.
+ /// </summary>
+ /// <value><c>true</c> if this instance is vf; otherwise, <c>false</c>.</value>
+ 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));
+
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is physical root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
+ public bool IsPhysicalRoot
+ {
+ get
+ {
+ return IsDirectory && Path.Equals(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
+ public bool IsRoot
+ {
+ get
+ {
+ return Parent == null;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the additional locations.
+ /// </summary>
+ /// <value>The additional locations.</value>
+ private List<string> AdditionalLocations { get; set; }
+
+ /// <summary>
+ /// Adds the additional location.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddAdditionalLocation(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (AdditionalLocations == null)
+ {
+ AdditionalLocations = new List<string>();
+ }
+
+ AdditionalLocations.Add(path);
+ }
+
+ /// <summary>
+ /// Gets the physical locations.
+ /// </summary>
+ /// <value>The physical locations.</value>
+ public IEnumerable<string> PhysicalLocations
+ {
+ get
+ {
+ var paths = string.IsNullOrWhiteSpace(Path) ? new string[] {} : new[] {Path};
+ return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations);
+ }
+ }
+
+ /// <summary>
+ /// Store these to reduce disk access in Resolvers
+ /// </summary>
+ /// <value>The metadata file dictionary.</value>
+ private Dictionary<string, WIN32_FIND_DATA> MetadataFileDictionary { get; set; }
+
+ /// <summary>
+ /// Gets the metadata files.
+ /// </summary>
+ /// <value>The metadata files.</value>
+ public IEnumerable<WIN32_FIND_DATA> MetadataFiles
+ {
+ get
+ {
+ if (MetadataFileDictionary != null)
+ {
+ return MetadataFileDictionary.Values;
+ }
+
+ return new WIN32_FIND_DATA[] {};
+ }
+ }
+
+ /// <summary>
+ /// Adds the metadata file.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.IO.FileNotFoundException"></exception>
+ public void AddMetadataFile(string path)
+ {
+ var file = FileSystem.GetFileData(path);
+
+ if (!file.HasValue)
+ {
+ throw new FileNotFoundException(path);
+ }
+
+ AddMetadataFile(file.Value);
+ }
+
+ /// <summary>
+ /// Adds the metadata file.
+ /// </summary>
+ /// <param name="fileInfo">The file info.</param>
+ public void AddMetadataFile(WIN32_FIND_DATA fileInfo)
+ {
+ AddMetadataFiles(new[] { fileInfo });
+ }
+
+ /// <summary>
+ /// Adds the metadata files.
+ /// </summary>
+ /// <param name="files">The files.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddMetadataFiles(IEnumerable<WIN32_FIND_DATA> files)
+ {
+ if (files == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (MetadataFileDictionary == null)
+ {
+ MetadataFileDictionary = new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
+ }
+ foreach (var file in files)
+ {
+ MetadataFileDictionary[file.cFileName] = file;
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the file system entry by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public WIN32_FIND_DATA? GetFileSystemEntryByName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name));
+ }
+
+ /// <summary>
+ /// Gets the file system entry by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the meta file by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Gets the name of the meta file by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Determines whether [contains meta file by name] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [contains meta file by name] [the specified name]; otherwise, <c>false</c>.</returns>
+ public bool ContainsMetaFileByName(string name)
+ {
+ return GetMetaFileByName(name).HasValue;
+ }
+
+ /// <summary>
+ /// Determines whether [contains file system entry by name] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
+ public bool ContainsFileSystemEntryByName(string name)
+ {
+ return GetFileSystemEntryByName(name).HasValue;
+ }
+
+ #region Equality Overrides
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ /// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return (Equals(obj as ItemResolveArgs));
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
+ public override int GetHashCode()
+ {
+ return Path.GetHashCode();
+ }
+
+ /// <summary>
+ /// Equalses the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ 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 32b8783df..000000000
--- 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
-{
- /// <summary>
- /// This is an EventArgs object used when resolving a Path into a BaseItem
- /// </summary>
- public class ItemResolveEventArgs : PreBeginResolveEventArgs
- {
- public WIN32_FIND_DATA[] FileSystemChildren { get; set; }
-
- protected List<string> _additionalLocations = new List<string>();
- public List<string> AdditionalLocations
- {
- get
- {
- return _additionalLocations;
- }
- set
- {
- _additionalLocations = value;
- }
- }
-
- public IEnumerable<string> PhysicalLocations
- {
- get
- {
- return (new List<string>() {this.Path}).Concat(AdditionalLocations);
- }
- }
-
- public bool IsBDFolder { get; set; }
- public bool IsDVDFolder { get; set; }
- public bool IsHDDVDFolder { get; set; }
-
- /// <summary>
- /// Store these to reduce disk access in Resolvers
- /// </summary>
- 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);
- }
- }
-
- /// <summary>
- /// 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
- /// </summary>
- 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 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
+{
+ /// <summary>
+ /// Class LibraryManager
+ /// </summary>
+ public class LibraryManager : BaseManager<Kernel>
+ {
+ #region LibraryChanged Event
+ /// <summary>
+ /// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
+ /// *** Will fire asynchronously. ***
+ /// </summary>
+ public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
+
+ /// <summary>
+ /// Raises the <see cref="E:LibraryChanged" /> event.
+ /// </summary>
+ /// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Sends the library changed web socket message.
+ /// </summary>
+ /// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
+ private void SendLibraryChangedWebSocketMessage(ChildrenChangedEventArgs args)
+ {
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("LibraryChanged", () => DtoBuilder.GetLibraryUpdateInfo(args));
+ }
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public LibraryManager(Kernel kernel)
+ : base(kernel)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the item.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ public BaseItem ResolveItem(ItemResolveArgs args)
+ {
+ return Kernel.EntityResolvers.Select(r => r.ResolvePath(args)).FirstOrDefault(i => i != null);
+ }
+
+ /// <summary>
+ /// Resolves a path into a BaseItem
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="fileInfo">The file info.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Resolves a set of files into a list of BaseItem
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="files">The files.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns>List{``0}.</returns>
+ public List<T> GetItems<T>(IEnumerable<WIN32_FIND_DATA> files, Folder parent)
+ where T : BaseItem
+ {
+ var list = new List<T>();
+
+ 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;
+ }
+
+ /// <summary>
+ /// Creates the root media folder
+ /// </summary>
+ /// <returns>AggregateFolder.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Person}.</returns>
+ public Task<Person> GetPerson(string name, bool allowSlowProviders = false)
+ {
+ return GetPerson(name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Person}.</returns>
+ private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Person>(Kernel.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Studio}.</returns>
+ public Task<Studio> GetStudio(string name, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Studio>(Kernel.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Genre}.</returns>
+ public Task<Genre> GetGenre(string name, bool allowSlowProviders = false)
+ {
+ return GetImagesByNameItem<Genre>(Kernel.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// The us culture
+ /// </summary>
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{Year}.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException"></exception>
+ public Task<Year> GetYear(int value, bool allowSlowProviders = false)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return GetImagesByNameItem<Year>(Kernel.ApplicationPaths.YearPath, value.ToString(UsCulture), CancellationToken.None, allowSlowProviders);
+ }
+
+ /// <summary>
+ /// The images by name item cache
+ /// </summary>
+ private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Generically retrieves an IBN item
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="path">The path.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{``0}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ private Task<T> GetImagesByNameItem<T>(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<T>(path, name, cancellationToken, allowSlowProviders));
+
+ return obj as Task<T>;
+ }
+
+ /// <summary>
+ /// Creates an IBN item based on a given path
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="path">The path.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{``0}.</returns>
+ /// <exception cref="System.IO.IOException">Path not created: + path</exception>
+ private async Task<T> CreateImagesByNameItem<T>(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;
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ // Clear the IBN cache
+ ImagesByNameItemCache.Clear();
+
+ const int maxTasks = 250;
+
+ var tasks = new List<Task>();
+
+ 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");
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ internal async Task ValidateMediaLibrary(IProgress<TaskProgress> 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<TaskProgress> { }, 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<TaskProgress> { }, 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<TaskProgress> { }, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Saves display preferences for a Folder
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="folder">The folder.</param>
+ /// <param name="data">The data.</param>
+ /// <returns>Task.</returns>
+ 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<Folder>()
+ .Where(i => i.DisplayPrefsId == folder.DisplayPrefsId))
+ {
+ child.AddOrUpdateDisplayPrefs(user, data);
+ }
+
+ return Kernel.DisplayPreferencesRepository.SaveDisplayPrefs(folder, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Gets the default view.
+ /// </summary>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetDefaultVirtualFolders()
+ {
+ return GetView(Kernel.ApplicationPaths.DefaultUserViewsPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetVirtualFolders(User user)
+ {
+ return GetView(user.RootFolderPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ private IEnumerable<VirtualFolderInfo> 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 000000000..4daa9d654
--- /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
+{
+ /// <summary>
+ /// Class Profiler
+ /// </summary>
+ public class Profiler : IDisposable
+ {
+ /// <summary>
+ /// The name
+ /// </summary>
+ readonly string name;
+ /// <summary>
+ /// The stopwatch
+ /// </summary>
+ readonly Stopwatch stopwatch;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Profiler" /> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public Profiler(string name)
+ {
+ this.name = name;
+
+ stopwatch = new Stopwatch();
+ stopwatch.Start();
+ }
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ 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 000000000..3e4d53204
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ResourcePool.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// This is just a collection of semaphores to control the number of concurrent executions of various resources
+ /// </summary>
+ public class ResourcePool : IDisposable
+ {
+ /// <summary>
+ /// You tube
+ /// </summary>
+ public readonly SemaphoreSlim YouTube = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The trakt
+ /// </summary>
+ public readonly SemaphoreSlim Trakt = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The tv db
+ /// </summary>
+ public readonly SemaphoreSlim TvDb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The movie db
+ /// </summary>
+ public readonly SemaphoreSlim MovieDb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The fan art
+ /// </summary>
+ public readonly SemaphoreSlim FanArt = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// The mb
+ /// </summary>
+ public readonly SemaphoreSlim Mb = new SemaphoreSlim(5, 5);
+
+ /// <summary>
+ /// Apple doesn't seem to like too many simulataneous requests.
+ /// </summary>
+ public readonly SemaphoreSlim AppleTrailerVideos = new SemaphoreSlim(1, 1);
+
+ /// <summary>
+ /// The apple trailer images
+ /// </summary>
+ public readonly SemaphoreSlim AppleTrailerImages = new SemaphoreSlim(1, 1);
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ 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 000000000..dfa80483e
--- /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
+{
+ /// <summary>
+ /// Class UserDataManager
+ /// </summary>
+ public class UserDataManager : BaseManager<Kernel>
+ {
+ #region Events
+ /// <summary>
+ /// Occurs when [playback start].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
+ /// <summary>
+ /// Occurs when [playback progress].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+ /// <summary>
+ /// Occurs when [playback stopped].
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped;
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDataManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public UserDataManager(Kernel kernel)
+ : base(kernel)
+ {
+
+ }
+
+ /// <summary>
+ /// Used to report that playback has started for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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
+ });
+ }
+
+ /// <summary>
+ /// Used to report playback progress for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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
+ });
+ }
+
+ /// <summary>
+ /// Used to report that playback has ended for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ 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
+ });
+ }
+
+ /// <summary>
+ /// Updates playstate position for an item but does not save
+ /// </summary>
+ /// <param name="item">The item</param>
+ /// <param name="data">User data for the item</param>
+ /// <param name="positionTicks">The current playback position</param>
+ /// <param name="incrementPlayCount">Whether or not to increment playcount</param>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Saves user data for an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="data">The data.</param>
+ 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 000000000..af3239657
--- /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
+{
+ /// <summary>
+ /// Class UserManager
+ /// </summary>
+ public class UserManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// The _active connections
+ /// </summary>
+ private readonly ConcurrentBag<ClientConnectionInfo> _activeConnections =
+ new ConcurrentBag<ClientConnectionInfo>();
+
+ /// <summary>
+ /// Gets all connections.
+ /// </summary>
+ /// <value>All connections.</value>
+ public IEnumerable<ClientConnectionInfo> AllConnections
+ {
+ get { return _activeConnections.Where(c => Kernel.GetUserById(c.UserId) != null).OrderByDescending(c => c.LastActivityDate); }
+ }
+
+ /// <summary>
+ /// Gets the active connections.
+ /// </summary>
+ /// <value>The active connections.</value>
+ public IEnumerable<ClientConnectionInfo> ActiveConnections
+ {
+ get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 10); }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public UserManager(Kernel kernel)
+ : base(kernel)
+ {
+ }
+
+ #region UserUpdated Event
+ /// <summary>
+ /// Occurs when [user updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserUpdated;
+
+ /// <summary>
+ /// Called when [user updated].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ internal void OnUserUpdated(User user)
+ {
+ EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs<User> { Argument = user });
+
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("UserUpdated", DtoBuilder.GetDtoUser(user));
+ }
+ #endregion
+
+ #region UserDeleted Event
+ /// <summary>
+ /// Occurs when [user deleted].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserDeleted;
+ /// <summary>
+ /// Called when [user deleted].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ internal void OnUserDeleted(User user)
+ {
+ EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user });
+
+ // Notify connected ui's
+ Kernel.TcpManager.SendWebSocketMessage("UserDeleted", user.Id.ToString());
+ }
+ #endregion
+
+ /// <summary>
+ /// Authenticates a User and returns a result indicating whether or not it succeeded
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="password">The password.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public async Task<bool> 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;
+ }
+
+ /// <summary>
+ /// Logs the user activity.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Updates the now playing item id.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="currentPositionTicks">The current position ticks.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Removes the now playing item id.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="item">The item.</param>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Logs the connection.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="lastActivityDate">The last activity date.</param>
+ private void LogConnection(Guid userId, ClientType clientType, string deviceName, DateTime lastActivityDate)
+ {
+ GetConnection(userId, clientType, deviceName).LastActivityDate = lastActivityDate;
+ }
+
+ /// <summary>
+ /// Gets the connection.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="clientType">Type of the client.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <returns>ClientConnectionInfo.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Loads the users from the repository
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ internal IEnumerable<User> 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;
+ }
+
+ /// <summary>
+ /// Refreshes metadata for each user
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <returns>Task.</returns>
+ public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
+ {
+ var tasks = Kernel.Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList();
+
+ return Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Renames the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newName">The new name.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Updates the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ 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);
+ }
+
+ /// <summary>
+ /// Creates the user.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ /// <exception cref="System.ArgumentNullException">name</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public async Task<User> 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;
+ }
+
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Instantiates the new user.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ private User InstantiateNewUser(string name)
+ {
+ return new User
+ {
+ Name = name,
+ Id = ("MBUser" + name).GetMD5(),
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow
+ };
+ }
+ }
+}