using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using MoreLinq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Dto { public class DtoService : IDtoService { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IUserDataRepository _userDataRepository; private readonly IItemRepository _itemRepo; public DtoService(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataRepository userDataRepository, IItemRepository itemRepo) { _logger = logger; _libraryManager = libraryManager; _userManager = userManager; _userDataRepository = userDataRepository; _itemRepo = itemRepo; } /// /// Converts a BaseItem to a DTOBaseItem /// /// The item. /// The fields. /// The user. /// The owner. /// Task{DtoBaseItem}. /// item public async Task GetBaseItemDto(BaseItem item, List fields, User user = null, BaseItem owner = null) { if (item == null) { throw new ArgumentNullException("item"); } if (fields == null) { throw new ArgumentNullException("fields"); } var dto = new BaseItemDto(); var tasks = new List(); if (fields.Contains(ItemFields.Studios)) { tasks.Add(AttachStudios(dto, item)); } if (fields.Contains(ItemFields.People)) { tasks.Add(AttachPeople(dto, item)); } if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) { try { await AttachPrimaryImageAspectRatio(dto, item).ConfigureAwait(false); } 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.DisplayPreferencesId)) { dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N"); } if (user != null) { AttachUserSpecificInfo(dto, item, user, fields); } AttachBasicFields(dto, item, owner, fields); if (fields.Contains(ItemFields.SoundtrackIds)) { dto.SoundtrackIds = item.SoundtrackIds .Select(i => i.ToString("N")) .ToArray(); } // 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 void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, List fields) { if (item.IsFolder) { var hasItemCounts = fields.Contains(ItemFields.ItemCounts); if (hasItemCounts || fields.Contains(ItemFields.CumulativeRunTimeTicks)) { var folder = (Folder)item; if (hasItemCounts) { dto.ChildCount = folder.GetChildren(user, true).Count(); } SetSpecialCounts(folder, user, dto); } } var userData = _userDataRepository.GetUserData(user.Id, item.GetUserDataKey()); dto.UserData = GetUserItemDataDto(userData); if (item.IsFolder) { dto.UserData.Played = dto.PlayedPercentage.HasValue && dto.PlayedPercentage.Value >= 100; } } public async Task GetUserDto(User user) { if (user == null) { throw new ArgumentNullException("user"); } var dto = new UserDto { Id = user.Id.ToString("N"), 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); try { await AttachPrimaryImageAspectRatio(dto, user).ConfigureAwait(false); } 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, user.Name); } } return dto; } public SessionInfoDto GetSessionInfoDto(SessionInfo session) { var dto = new SessionInfoDto { Client = session.Client, DeviceId = session.DeviceId, DeviceName = session.DeviceName, Id = session.Id.ToString("N"), LastActivityDate = session.LastActivityDate, NowPlayingPositionTicks = session.NowPlayingPositionTicks, SupportsRemoteControl = session.SupportsRemoteControl, IsPaused = session.IsPaused, IsMuted = session.IsMuted, NowViewingContext = session.NowViewingContext, NowViewingItemId = session.NowViewingItemId, NowViewingItemName = session.NowViewingItemName, NowViewingItemType = session.NowViewingItemType, ApplicationVersion = session.ApplicationVersion }; if (session.NowPlayingItem != null) { dto.NowPlayingItem = GetBaseItemInfo(session.NowPlayingItem); } if (session.User != null) { dto.UserId = session.User.Id.ToString("N"); dto.UserName = session.User.Name; } return dto; } /// /// Converts a BaseItem to a BaseItemInfo /// /// The item. /// BaseItemInfo. /// item public BaseItemInfo GetBaseItemInfo(BaseItem item) { if (item == null) { throw new ArgumentNullException("item"); } var info = new BaseItemInfo { Id = GetDtoId(item), Name = item.Name, MediaType = item.MediaType, Type = item.GetType().Name, IsFolder = item.IsFolder, RunTimeTicks = item.RunTimeTicks }; var imagePath = item.PrimaryImagePath; if (!string.IsNullOrEmpty(imagePath)) { try { info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath); } catch (IOException) { } } return info; } const string IndexFolderDelimeter = "-index-"; /// /// Gets client-side Id of a server-side BaseItem /// /// The item. /// System.String. /// item public string GetDtoId(BaseItem item) { if (item == null) { throw new ArgumentNullException("item"); } var indexFolder = item as IndexFolder; if (indexFolder != null) { return GetDtoId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id; } return item.Id.ToString("N"); } /// /// Converts a UserItemData to a DTOUserItemData /// /// The data. /// DtoUserItemData. /// public UserItemDataDto GetUserItemDataDto(UserItemData data) { if (data == null) { throw new ArgumentNullException("data"); } return new UserItemDataDto { IsFavorite = data.IsFavorite, Likes = data.Likes, PlaybackPositionTicks = data.PlaybackPositionTicks, PlayCount = data.PlayCount, Rating = data.Rating, Played = data.Played, LastPlayedDate = data.LastPlayedDate }; } private void SetBookProperties(BaseItemDto dto, Book item) { dto.SeriesName = item.SeriesName; } private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item) { if (!string.IsNullOrEmpty(item.Album)) { var parentAlbum = _libraryManager.RootFolder .RecursiveChildren .OfType() .FirstOrDefault(i => string.Equals(i.Name, item.Album, StringComparison.OrdinalIgnoreCase)); if (parentAlbum != null) { dto.AlbumId = GetDtoId(parentAlbum); } } dto.Album = item.Album; dto.Artists = string.IsNullOrEmpty(item.Artist) ? new string[] { } : new[] { item.Artist }; } private void SetGameProperties(BaseItemDto dto, Game item) { dto.Players = item.PlayersSupported; dto.GameSystem = item.GameSystem; } /// /// Gets the backdrop image tags. /// /// The item. /// List{System.String}. private List GetBackdropImageTags(BaseItem item) { return item.BackdropImagePaths .Select(p => GetImageCacheTag(item, ImageType.Backdrop, p)) .Where(i => i.HasValue) .Select(i => i.Value) .ToList(); } /// /// Gets the screenshot image tags. /// /// The item. /// List{Guid}. private List GetScreenshotImageTags(BaseItem item) { return item.ScreenshotImagePaths .Select(p => GetImageCacheTag(item, ImageType.Screenshot, p)) .Where(i => i.HasValue) .Select(i => i.Value) .ToList(); } private Guid? GetImageCacheTag(BaseItem item, ImageType type, string path) { try { return Kernel.Instance.ImageManager.GetImageCacheTag(item, type, path); } catch (IOException ex) { _logger.ErrorException("Error getting {0} image info for {1}", ex, type, path); return null; } } /// /// Attaches People DTO's to a DTOBaseItem /// /// The dto. /// The item. /// Task. private async Task AttachPeople(BaseItemDto dto, BaseItem item) { // Ordering by person type to ensure actors and artists are at the front. // This is taking advantage of the fact that they both begin with A // This should be improved in the future var people = item.People.OrderBy(i => i.Type).ToList(); // Attach People by transforming them into BaseItemPerson (DTO) dto.People = new BaseItemPerson[people.Count]; var entities = await Task.WhenAll(people.Select(p => p.Name) .Distinct(StringComparer.OrdinalIgnoreCase).Select(c => Task.Run(async () => { try { return await _libraryManager.GetPerson(c).ConfigureAwait(false); } catch (IOException ex) { _logger.ErrorException("Error getting person {0}", ex, c); return null; } }) )).ConfigureAwait(false); var dictionary = entities.Where(i => i != null) .DistinctBy(i => i.Name) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < people.Count; i++) { var person = people[i]; var baseItemPerson = new BaseItemPerson { Name = person.Name, Role = person.Role, Type = person.Type }; Person entity; if (dictionary.TryGetValue(person.Name, out entity)) { var primaryImagePath = entity.PrimaryImagePath; if (!string.IsNullOrEmpty(primaryImagePath)) { baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary, primaryImagePath); } } dto.People[i] = baseItemPerson; } } /// /// Attaches the studios. /// /// The dto. /// The item. /// Task. private async Task AttachStudios(BaseItemDto dto, BaseItem item) { var studios = item.Studios.ToList(); dto.Studios = new StudioDto[studios.Count]; var entities = await Task.WhenAll(studios.Distinct(StringComparer.OrdinalIgnoreCase).Select(c => Task.Run(async () => { try { return await _libraryManager.GetStudio(c).ConfigureAwait(false); } catch (IOException ex) { _logger.ErrorException("Error getting studio {0}", ex, c); return null; } }) )).ConfigureAwait(false); var dictionary = entities .Where(i => i != null) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < studios.Count; i++) { var studio = studios[i]; var studioDto = new StudioDto { Name = studio }; Studio entity; if (dictionary.TryGetValue(studio, out entity)) { var primaryImagePath = entity.PrimaryImagePath; if (!string.IsNullOrEmpty(primaryImagePath)) { studioDto.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary, primaryImagePath); } } dto.Studios[i] = studioDto; } } /// /// If an item does not any backdrops, this can be used to find the first parent that does have one /// /// The item. /// The owner. /// BaseItem. private BaseItem GetParentBackdropItem(BaseItem item, BaseItem owner) { var parent = item.Parent ?? owner; 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. /// The type. /// The owner. /// BaseItem. private BaseItem GetParentImageItem(BaseItem item, ImageType type, BaseItem owner) { var parent = item.Parent ?? owner; while (parent != null) { if (parent.HasImage(type)) { return parent; } parent = parent.Parent; } return null; } /// /// Gets the chapter info dto. /// /// The chapter info. /// The item. /// ChapterInfoDto. private ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item) { var dto = new ChapterInfoDto { Name = chapterInfo.Name, StartPositionTicks = chapterInfo.StartPositionTicks }; if (!string.IsNullOrEmpty(chapterInfo.ImagePath)) { dto.ImageTag = GetImageCacheTag(item, ImageType.Chapter, chapterInfo.ImagePath); } return dto; } /// /// Gets a BaseItem based upon it's client-side item id /// /// The id. /// The user id. /// BaseItem. public BaseItem GetItemByDtoId(string id, Guid? userId = null) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } // If the item is an indexed folder we have to do a special routine to get it var isIndexFolder = id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1; if (isIndexFolder) { if (userId.HasValue) { return GetIndexFolder(id, userId.Value); } } BaseItem item = null; if (userId.HasValue || !isIndexFolder) { item = _libraryManager.GetItemById(new Guid(id)); } // If we still don't find it, look within individual user views if (item == null && !userId.HasValue && isIndexFolder) { foreach (var user in _userManager.Users) { item = GetItemByDtoId(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 BaseItem GetIndexFolder(string id, Guid userId) { var user = _userManager.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 = GetItemByDtoId(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 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, false, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder; // Nested index folder if (values.Count > 0) { return GetIndexFolder(values, indexFolder, user); } return indexFolder; } /// /// Sets simple property values on a DTOBaseItem /// /// The dto. /// The item. /// The owner. /// The fields. private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, List fields) { if (fields.Contains(ItemFields.DateCreated)) { dto.DateCreated = item.DateCreated; } if (fields.Contains(ItemFields.OriginalRunTimeTicks)) { dto.OriginalRunTimeTicks = item.OriginalRunTimeTicks; } dto.DisplayMediaType = item.DisplayMediaType; if (fields.Contains(ItemFields.MetadataSettings)) { dto.LockedFields = item.LockedFields; dto.EnableInternetProviders = !item.DontFetchMeta; } if (fields.Contains(ItemFields.Budget)) { dto.Budget = item.Budget; } if (fields.Contains(ItemFields.Revenue)) { dto.Revenue = item.Revenue; } dto.EndDate = item.EndDate; if (fields.Contains(ItemFields.HomePageUrl)) { dto.HomePageUrl = item.HomePageUrl; } if (fields.Contains(ItemFields.Tags)) { dto.Tags = item.Tags; } if (fields.Contains(ItemFields.ProductionLocations)) { dto.ProductionLocations = item.ProductionLocations; } dto.AspectRatio = item.AspectRatio; dto.BackdropImageTags = GetBackdropImageTags(item); dto.ScreenshotImageTags = GetScreenshotImageTags(item); if (fields.Contains(ItemFields.Genres)) { dto.Genres = item.Genres; } dto.ImageTags = new Dictionary(); foreach (var image in item.Images) { var type = image.Key; var tag = GetImageCacheTag(item, type, image.Value); if (tag.HasValue) { dto.ImageTags[type] = tag.Value; } } dto.Id = GetDtoId(item); dto.IndexNumber = item.IndexNumber; dto.IsFolder = item.IsFolder; dto.Language = item.Language; dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; dto.CriticRating = item.CriticRating; if (fields.Contains(ItemFields.CriticRatingSummary)) { dto.CriticRatingSummary = item.CriticRatingSummary; } var localTrailerCount = item.LocalTrailerIds.Count; if (localTrailerCount > 0) { dto.LocalTrailerCount = localTrailerCount; } dto.Name = item.Name; dto.OfficialRating = item.OfficialRating; var hasOverview = fields.Contains(ItemFields.Overview); var hasHtmlOverview = fields.Contains(ItemFields.OverviewHtml); if (hasOverview || hasHtmlOverview) { var strippedOverview = string.IsNullOrEmpty(item.Overview) ? item.Overview : item.Overview.StripHtml(); if (hasOverview) { dto.Overview = strippedOverview; } // Only supply the html version if there was actually html content if (hasHtmlOverview) { dto.OverviewHtml = 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, owner); if (parentWithBackdrop != null) { dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop); dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop); } } if (item.Parent != null && fields.Contains(ItemFields.ParentId)) { dto.ParentId = GetDtoId(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 = GetParentImageItem(item, ImageType.Logo, owner); if (parentWithLogo != null) { dto.ParentLogoItemId = GetDtoId(parentWithLogo); dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo)); } } // If there is no art, indicate what parent has one in case the Ui wants to allow inheritance if (!dto.HasArtImage) { var parentWithImage = GetParentImageItem(item, ImageType.Art, owner); if (parentWithImage != null) { dto.ParentArtItemId = GetDtoId(parentWithImage); dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art, parentWithImage.GetImage(ImageType.Art)); } } 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.CustomRating)) { dto.CustomRating = item.CustomRating; } if (fields.Contains(ItemFields.Taglines)) { dto.Taglines = item.Taglines; } if (fields.Contains(ItemFields.RemoteTrailers)) { dto.RemoteTrailers = item.RemoteTrailers; } dto.Type = item.GetType().Name; dto.CommunityRating = item.CommunityRating; if (item.IsFolder) { var folder = (Folder)item; if (fields.Contains(ItemFields.IndexOptions)) { dto.IndexOptions = folder.IndexByOptionStrings.ToArray(); } } // Add audio info var audio = item as Audio; if (audio != null) { dto.Album = audio.Album; dto.AlbumArtist = audio.AlbumArtist; dto.Artists = new[] { audio.Artist }; var albumParent = audio.FindParent(); if (albumParent != null) { dto.AlbumId = GetDtoId(albumParent); var imagePath = albumParent.PrimaryImagePath; if (!string.IsNullOrEmpty(imagePath)) { dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary, imagePath); } } } var album = item as MusicAlbum; if (album != null) { var songs = album.RecursiveChildren.OfType