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 MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { /// /// Generates DTO's from domain entities /// public static class DtoBuilder { /// /// The index folder delimeter /// const string IndexFolderDelimeter = "-index-"; private static ILogger Logger = LogManager.GetLogger("DtoBuilder"); /// /// Gets the dto base item. /// /// The item. /// The fields. /// Task{DtoBaseItem}. /// item public async static Task GetDtoBaseItem(BaseItem item, List fields) { if (item == null) { throw new ArgumentNullException("item"); } if (fields == null) { throw new ArgumentNullException("fields"); } var dto = new DtoBaseItem(); var tasks = new List(); if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) { try { tasks.Add(AttachPrimaryImageAspectRatio(dto, item)); } catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions Logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name); } } if (fields.Contains(ItemFields.Studios)) { dto.Studios = item.Studios; } if (fields.Contains(ItemFields.People)) { tasks.Add(AttachPeople(dto, item)); } AttachBasicFields(dto, item, fields); // Make sure all the tasks we kicked off have completed. if (tasks.Count > 0) { await Task.WhenAll(tasks).ConfigureAwait(false); } return dto; } /// /// Converts a BaseItem to a DTOBaseItem /// /// The item. /// The user. /// The fields. /// Task{DtoBaseItem}. /// public async static Task GetDtoBaseItem(BaseItem item, User user, List fields) { if (item == null) { throw new ArgumentNullException("item"); } if (user == null) { throw new ArgumentNullException("user"); } if (fields == null) { throw new ArgumentNullException("fields"); } var dto = new DtoBaseItem(); var tasks = new List(); if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) { try { tasks.Add(AttachPrimaryImageAspectRatio(dto, item)); } catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions Logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name); } } if (fields.Contains(ItemFields.Studios)) { dto.Studios = item.Studios; } if (fields.Contains(ItemFields.People)) { tasks.Add(AttachPeople(dto, item)); } AttachBasicFields(dto, item, fields); AttachUserSpecificInfo(dto, item, user, fields); // Make sure all the tasks we kicked off have completed. if (tasks.Count > 0) { await Task.WhenAll(tasks).ConfigureAwait(false); } return dto; } /// /// Attaches the user specific info. /// /// The dto. /// The item. /// The user. /// The fields. private static void AttachUserSpecificInfo(DtoBaseItem dto, BaseItem item, User user, List fields) { dto.IsNew = item.IsRecentlyAdded(user); if (fields.Contains(ItemFields.UserData)) { var userData = item.GetUserData(user, false); if (userData != null) { dto.UserData = GetDtoUserItemData(userData); } } if (item.IsFolder && fields.Contains(ItemFields.DisplayPreferences)) { dto.DisplayPreferences = ((Folder)item).GetDisplayPrefs(user, false) ?? new DisplayPreferences { UserId = user.Id }; } if (item.IsFolder) { if (fields.Contains(ItemFields.ItemCounts)) { var folder = (Folder)item; // Skip sorting since all we want is a count dto.ChildCount = folder.GetChildren(user).Count(); SetSpecialCounts(folder, user, dto); } } } /// /// Attaches the primary image aspect ratio. /// /// The dto. /// The item. /// Task. private static async Task AttachPrimaryImageAspectRatio(DtoBaseItem dto, BaseItem item) { var path = item.PrimaryImagePath; if (string.IsNullOrEmpty(path)) { return; } var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(path); // See if we can avoid a file system lookup by looking for the file in ResolveArgs var dateModified = metaFileEntry == null ? File.GetLastWriteTimeUtc(path) : metaFileEntry.Value.LastWriteTimeUtc; ImageSize size; try { size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false); } catch (FileNotFoundException) { Logger.Error("Image file does not exist: {0}", path); return; } foreach (var enhancer in Kernel.Instance.ImageEnhancers .Where(i => i.Supports(item, ImageType.Primary))) { size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size); } dto.PrimaryImageAspectRatio = size.Width / size.Height; } /// /// Sets simple property values on a DTOBaseItem /// /// The dto. /// The item. /// The fields. private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, List fields) { if (fields.Contains(ItemFields.DateCreated)) { dto.DateCreated = item.DateCreated; } if (fields.Contains(ItemFields.DisplayMediaType)) { dto.DisplayMediaType = item.DisplayMediaType; } dto.AspectRatio = item.AspectRatio; dto.BackdropImageTags = GetBackdropImageTags(item); if (fields.Contains(ItemFields.Genres)) { dto.Genres = item.Genres; } if (item.Images != null) { dto.ImageTags = new Dictionary(); foreach (var image in item.Images) { ImageType type; if (Enum.TryParse(image.Key, true, out type)) { dto.ImageTags[type] = Kernel.Instance.ImageManager.GetImageCacheTag(item, type, image.Value); } } } dto.Id = GetClientItemId(item); dto.IndexNumber = item.IndexNumber; dto.IsFolder = item.IsFolder; dto.Language = item.Language; dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; var localTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count; if (localTrailerCount > 0) { dto.LocalTrailerCount = localTrailerCount; } dto.Name = item.Name; dto.OfficialRating = item.OfficialRating; if (fields.Contains(ItemFields.Overview)) { dto.Overview = item.Overview; } // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance if (dto.BackdropImageTags.Count == 0) { var parentWithBackdrop = GetParentBackdropItem(item); if (parentWithBackdrop != null) { dto.ParentBackdropItemId = GetClientItemId(parentWithBackdrop); dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop); } } if (item.Parent != null && fields.Contains(ItemFields.ParentId)) { dto.ParentId = GetClientItemId(item.Parent); } dto.ParentIndexNumber = item.ParentIndexNumber; // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance if (!dto.HasLogo) { var parentWithLogo = GetParentLogoItem(item); if (parentWithLogo != null) { dto.ParentLogoItemId = GetClientItemId(parentWithLogo); dto.ParentLogoImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo)); } } if (fields.Contains(ItemFields.Path)) { dto.Path = item.Path; } dto.PremiereDate = item.PremiereDate; dto.ProductionYear = item.ProductionYear; if (fields.Contains(ItemFields.ProviderIds)) { dto.ProviderIds = item.ProviderIds; } dto.RunTimeTicks = item.RunTimeTicks; if (fields.Contains(ItemFields.SortName)) { dto.SortName = item.SortName; } if (fields.Contains(ItemFields.Taglines)) { dto.Taglines = item.Taglines; } if (fields.Contains(ItemFields.TrailerUrls)) { dto.TrailerUrls = item.TrailerUrls; } dto.Type = item.GetType().Name; dto.CommunityRating = item.CommunityRating; if (item.IsFolder) { var folder = (Folder)item; dto.IsRoot = folder.IsRoot; dto.IsVirtualFolder = folder.IsVirtualFolder; if (fields.Contains(ItemFields.IndexOptions)) { dto.IndexOptions = folder.IndexByOptionStrings.ToArray(); } if (fields.Contains(ItemFields.SortOptions)) { dto.SortOptions = folder.SortByOptionStrings.ToArray(); } } // Add audio info var audio = item as Audio; if (audio != null) { if (fields.Contains(ItemFields.AudioInfo)) { dto.Album = audio.Album; dto.AlbumArtist = audio.AlbumArtist; dto.Artist = audio.Artist; } } // Add video info var video = item as Video; if (video != null) { dto.VideoType = video.VideoType; dto.VideoFormat = video.VideoFormat; dto.IsoType = video.IsoType; if (fields.Contains(ItemFields.Chapters) && video.Chapters != null) { dto.Chapters = video.Chapters.Select(c => GetChapterInfoDto(c, item)).ToList(); } } if (fields.Contains(ItemFields.MediaStreams)) { // Add VideoInfo var iHasMediaStreams = item as IHasMediaStreams; if (iHasMediaStreams != null) { dto.MediaStreams = iHasMediaStreams.MediaStreams; } } // Add MovieInfo var movie = item as Movie; if (movie != null) { var specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count; if (specialFeatureCount > 0) { dto.SpecialFeatureCount = specialFeatureCount; } } if (fields.Contains(ItemFields.SeriesInfo)) { // Add SeriesInfo var series = item as Series; if (series != null) { dto.AirDays = series.AirDays; dto.AirTime = series.AirTime; dto.Status = series.Status; } // Add EpisodeInfo var episode = item as Episode; if (episode != null) { series = item.FindParent(); dto.SeriesId = GetClientItemId(series); dto.SeriesName = series.Name; } // Add SeasonInfo var season = item as Season; if (season != null) { series = item.FindParent(); dto.SeriesId = GetClientItemId(series); dto.SeriesName = series.Name; } } } /// /// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once /// /// The folder. /// The user. /// The dto. private static void SetSpecialCounts(Folder folder, User user, DtoBaseItem dto) { var utcNow = DateTime.UtcNow; var rcentlyAddedItemCount = 0; var recursiveItemCount = 0; var favoriteItemsCount = 0; var recentlyAddedUnPlayedItemCount = 0; var resumableItemCount = 0; var recentlyPlayedItemCount = 0; double totalPercentPlayed = 0; // Loop through each recursive child foreach (var child in folder.GetRecursiveChildren(user)) { var userdata = child.GetUserData(user, false); if (!child.IsFolder) { recursiveItemCount++; // Check is recently added if (child.IsRecentlyAdded(user)) { rcentlyAddedItemCount++; // Check recently added unplayed if (userdata == null || userdata.PlayCount == 0) { recentlyAddedUnPlayedItemCount++; } } // Incrememt totalPercentPlayed if (userdata != null) { if (userdata.PlayCount > 0) { totalPercentPlayed += 100; } else if (userdata.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0) { double itemPercent = userdata.PlaybackPositionTicks; itemPercent /= child.RunTimeTicks.Value; totalPercentPlayed += itemPercent; } } } if (userdata != null) { if (userdata.IsFavorite) { favoriteItemsCount++; } if (userdata.PlaybackPositionTicks > 0) { resumableItemCount++; } if (userdata.LastPlayedDate.HasValue && (utcNow - userdata.LastPlayedDate.Value).TotalDays < Kernel.Instance.Configuration.RecentlyPlayedDays) { recentlyPlayedItemCount++; } } } dto.RecursiveItemCount = recursiveItemCount; dto.RecentlyAddedItemCount = rcentlyAddedItemCount; dto.RecentlyAddedUnPlayedItemCount = recentlyAddedUnPlayedItemCount; dto.ResumableItemCount = resumableItemCount; dto.FavoriteItemCount = favoriteItemsCount; dto.RecentlyPlayedItemCount = recentlyPlayedItemCount; if (recursiveItemCount > 0) { dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount; } } /// /// Attaches People DTO's to a DTOBaseItem /// /// The dto. /// The item. /// Task. private static async Task AttachPeople(DtoBaseItem dto, BaseItem item) { if (item.People == null) { return; } // Attach People by transforming them into BaseItemPerson (DTO) dto.People = new BaseItemPerson[item.People.Count]; var entities = await Task.WhenAll(item.People.Select(c => Task.Run(async () => { try { return await Kernel.Instance.LibraryManager.GetPerson(c.Name).ConfigureAwait(false); } catch (IOException ex) { Logger.ErrorException("Error getting person {0}", ex, c.Name); return null; } }) )).ConfigureAwait(false); for (var i = 0; i < item.People.Count; i++) { var person = item.People[i]; var baseItemPerson = new BaseItemPerson { Name = person.Name, Role = person.Role, Type = person.Type }; var ibnObject = entities[i]; if (ibnObject != null) { var primaryImagePath = ibnObject.PrimaryImagePath; if (!string.IsNullOrEmpty(primaryImagePath)) { baseItemPerson.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(ibnObject, ImageType.Primary, primaryImagePath); } } dto.People[i] = baseItemPerson; } } /// /// If an item does not any backdrops, this can be used to find the first parent that does have one /// /// The item. /// BaseItem. private static BaseItem GetParentBackdropItem(BaseItem item) { var parent = item.Parent; while (parent != null) { if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Count > 0) { return parent; } parent = parent.Parent; } return null; } /// /// If an item does not have a logo, this can be used to find the first parent that does have one /// /// The item. /// BaseItem. private static BaseItem GetParentLogoItem(BaseItem item) { var parent = item.Parent; while (parent != null) { if (parent.HasImage(ImageType.Logo)) { return parent; } parent = parent.Parent; } return null; } /// /// Gets the library update info. /// /// The instance containing the event data. /// LibraryUpdateInfo. internal static LibraryUpdateInfo GetLibraryUpdateInfo(ChildrenChangedEventArgs changeEvent) { return new LibraryUpdateInfo { Folder = GetBaseItemInfo(changeEvent.Folder), ItemsAdded = changeEvent.ItemsAdded.Select(GetBaseItemInfo), ItemsRemoved = changeEvent.ItemsRemoved.Select(i => i.Id), ItemsUpdated = changeEvent.ItemsUpdated.Select(i => i.Id) }; } /// /// Converts a UserItemData to a DTOUserItemData /// /// The data. /// DtoUserItemData. /// public static DtoUserItemData GetDtoUserItemData(UserItemData data) { if (data == null) { throw new ArgumentNullException(); } return new DtoUserItemData { IsFavorite = data.IsFavorite, Likes = data.Likes, PlaybackPositionTicks = data.PlaybackPositionTicks, PlayCount = data.PlayCount, Rating = data.Rating, Played = data.Played }; } /// /// Gets the chapter info dto. /// /// The chapter info. /// The item. /// ChapterInfoDto. private static ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item) { var dto = new ChapterInfoDto { Name = chapterInfo.Name, StartPositionTicks = chapterInfo.StartPositionTicks }; if (!string.IsNullOrEmpty(chapterInfo.ImagePath)) { dto.ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.ChapterImage, chapterInfo.ImagePath); } return dto; } /// /// Converts a BaseItem to a BaseItemInfo /// /// The item. /// BaseItemInfo. /// item public static BaseItemInfo GetBaseItemInfo(BaseItem item) { if (item == null) { throw new ArgumentNullException("item"); } var info = new BaseItemInfo { Id = GetClientItemId(item), Name = item.Name, Type = item.GetType().Name, IsFolder = item.IsFolder, RunTimeTicks = item.RunTimeTicks }; var imagePath = item.PrimaryImagePath; if (!string.IsNullOrEmpty(imagePath)) { info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath); } if (item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0) { imagePath = item.BackdropImagePaths[0]; if (!string.IsNullOrEmpty(imagePath)) { info.BackdropImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, imagePath); } } return info; } /// /// Gets client-side Id of a server-side BaseItem /// /// The item. /// System.String. /// item public static string GetClientItemId(BaseItem item) { if (item == null) { throw new ArgumentNullException("item"); } var indexFolder = item as IndexFolder; if (indexFolder != null) { return GetClientItemId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id; } return item.Id.ToString(); } /// /// Converts a User to a DTOUser /// /// The user. /// DtoUser. /// user public static DtoUser GetDtoUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } var dto = new DtoUser { Id = user.Id, Name = user.Name, HasPassword = !String.IsNullOrEmpty(user.Password), LastActivityDate = user.LastActivityDate, LastLoginDate = user.LastLoginDate, Configuration = user.Configuration }; var image = user.PrimaryImagePath; if (!string.IsNullOrEmpty(image)) { dto.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(user, ImageType.Primary, image); } return dto; } /// /// Gets a BaseItem based upon it's client-side item id /// /// The id. /// The user id. /// BaseItem. public static BaseItem GetItemByClientId(string id, Guid? userId = null) { var isIdEmpty = string.IsNullOrEmpty(id); // If the item is an indexed folder we have to do a special routine to get it var isIndexFolder = !isIdEmpty && id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1; if (isIndexFolder) { if (userId.HasValue) { return GetIndexFolder(id, userId.Value); } } BaseItem item = null; if (userId.HasValue) { item = isIdEmpty ? Kernel.Instance.GetUserById(userId.Value).RootFolder : Kernel.Instance.GetItemById(new Guid(id), userId.Value); } else if (!isIndexFolder) { item = Kernel.Instance.GetItemById(new Guid(id)); } // If we still don't find it, look within individual user views if (item == null && !userId.HasValue) { foreach (var user in Kernel.Instance.Users) { item = GetItemByClientId(id, user.Id); if (item != null) { break; } } } return item; } /// /// Finds an index folder based on an Id and userId /// /// The id. /// The user id. /// BaseItem. private static BaseItem GetIndexFolder(string id, Guid userId) { var user = Kernel.Instance.GetUserById(userId); var stringSeparators = new[] { IndexFolderDelimeter }; // Split using the delimeter var values = id.Split(stringSeparators, StringSplitOptions.None).ToList(); // Get the top folder normally using the first id var folder = GetItemByClientId(values[0], userId) as Folder; values.RemoveAt(0); // Get indexed folders using the remaining values in the id string return GetIndexFolder(values, folder, user); } /// /// Gets indexed folders based on a list of index names and folder id's /// /// The values. /// The parent folder. /// The user. /// BaseItem. private static BaseItem GetIndexFolder(List values, Folder parentFolder, User user) { // The index name is first var indexBy = values[0]; // The index folder id is next var indexFolderId = new Guid(values[1]); // Remove them from the lst values.RemoveRange(0, 2); // Get the IndexFolder var indexFolder = parentFolder.GetChildren(user, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder; // Nested index folder if (values.Count > 0) { return GetIndexFolder(values, indexFolder, user); } return indexFolder; } /// /// Gets the backdrop image tags. /// /// The item. /// List{System.String}. private static List GetBackdropImageTags(BaseItem item) { if (item.BackdropImagePaths == null) { return new List(); } return item.BackdropImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, p)).ToList(); } } }