diff options
| author | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
|---|---|---|
| committer | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
| commit | 767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch) | |
| tree | 49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/Library | |
| parent | 845554722efaed872948a9e0f7202e3ef52f1b6e (diff) | |
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/Library')
| -rw-r--r-- | MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs | 171 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/DtoBuilder.cs | 934 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ItemController.cs | 136 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ItemResolveArgs.cs | 397 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ItemResolveEventArgs.cs | 104 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/LibraryManager.cs | 511 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/Profiler.cs | 69 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/ResourcePool.cs | 79 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/UserDataManager.cs | 219 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Library/UserManager.cs | 395 |
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 + }; + } + } +} |
