diff options
Diffstat (limited to 'MediaBrowser.Api/UserLibrary')
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/ArtistsService.cs | 143 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs | 388 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs | 478 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/GenresService.cs | 140 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/ItemsService.cs | 514 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/PersonsService.cs | 146 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/PlaystateService.cs | 456 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/StudiosService.cs | 132 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 575 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/UserViewsService.cs | 148 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/YearsService.cs | 131 |
11 files changed, 3251 insertions, 0 deletions
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs new file mode 100644 index 000000000..9875e0208 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetArtists. + /// </summary> + [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")] + public class GetArtists : GetItemsByName + { + } + + [Route("/Artists/AlbumArtists", "GET", Summary = "Gets all album artists from a given item, folder, or the entire library")] + public class GetAlbumArtists : GetItemsByName + { + } + + [Route("/Artists/{Name}", "GET", Summary = "Gets an artist, by name")] + public class GetArtist : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class ArtistsService. + /// </summary> + [Authenticated] + public class ArtistsService : BaseItemsByNameService<MusicArtist> + { + public ArtistsService( + ILogger<ArtistsService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + userDataRepository, + dtoService, + authorizationContext) + { + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetArtist request) + { + return GetItem(request); + } + + /// <summary> + /// Gets the item. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{BaseItemDto}.</returns> + private BaseItemDto GetItem(GetArtist request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + var item = GetArtist(request.Name, LibraryManager, dtoOptions); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + + return DtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return DtoService.GetBaseItemDto(item, dtoOptions); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetArtists request) + { + return GetResultSlim(request); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetAlbumArtists request) + { + var result = GetResultSlim(request); + + return ToOptimizedResult(result); + } + + protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items) + { + throw new NotImplementedException(); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs new file mode 100644 index 000000000..fd639caf1 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -0,0 +1,388 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class BaseItemsByNameService. + /// </summary> + /// <typeparam name="TItemType">The type of the T item type.</typeparam> + public abstract class BaseItemsByNameService<TItemType> : BaseApiService + where TItemType : BaseItem, IItemByName + { + /// <summary> + /// Initializes a new instance of the <see cref="BaseItemsByNameService{TItemType}" /> class. + /// </summary> + /// <param name="userManager">The user manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="userDataRepository">The user data repository.</param> + /// <param name="dtoService">The dto service.</param> + protected BaseItemsByNameService( + ILogger<BaseItemsByNameService<TItemType>> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base(logger, serverConfigurationManager, httpResultFactory) + { + UserManager = userManager; + LibraryManager = libraryManager; + UserDataRepository = userDataRepository; + DtoService = dtoService; + AuthorizationContext = authorizationContext; + } + + /// <summary> + /// Gets the _user manager. + /// </summary> + protected IUserManager UserManager { get; } + + /// <summary> + /// Gets the library manager. + /// </summary> + protected ILibraryManager LibraryManager { get; } + + protected IUserDataManager UserDataRepository { get; } + + protected IDtoService DtoService { get; } + + protected IAuthorizationContext AuthorizationContext { get; } + + protected BaseItem GetParentItem(GetItemsByName request) + { + BaseItem parentItem; + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId); + } + else + { + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId); + } + + return parentItem; + } + + protected string GetParentItemViewType(GetItemsByName request) + { + var parent = GetParentItem(request); + + if (parent is IHasCollectionType collectionFolder) + { + return collectionFolder.CollectionType; + } + + return null; + } + + protected QueryResult<BaseItemDto> GetResultSlim(GetItemsByName request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + User user = null; + BaseItem parentItem; + + if (!request.UserId.Equals(Guid.Empty)) + { + user = UserManager.GetUserById(request.UserId); + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId); + } + else + { + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId); + } + + var excludeItemTypes = request.GetExcludeItemTypes(); + var includeItemTypes = request.GetIncludeItemTypes(); + var mediaTypes = request.GetMediaTypes(); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, + StartIndex = request.StartIndex, + Limit = request.Limit, + IsFavorite = request.IsFavorite, + NameLessThan = request.NameLessThan, + NameStartsWith = request.NameStartsWith, + NameStartsWithOrGreater = request.NameStartsWithOrGreater, + Tags = request.GetTags(), + OfficialRatings = request.GetOfficialRatings(), + Genres = request.GetGenres(), + GenreIds = GetGuids(request.GenreIds), + StudioIds = GetGuids(request.StudioIds), + Person = request.Person, + PersonIds = GetGuids(request.PersonIds), + PersonTypes = request.GetPersonTypes(), + Years = request.GetYears(), + MinCommunityRating = request.MinCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = request.SearchTerm, + EnableTotalRecordCount = request.EnableTotalRecordCount + }; + + if (!string.IsNullOrWhiteSpace(request.ParentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { new Guid(request.ParentId) }; + } + else + { + query.ItemIds = new[] { new Guid(request.ParentId) }; + } + } + + // Studios + if (!string.IsNullOrEmpty(request.Studios)) + { + query.StudioIds = request.Studios.Split('|').Select(i => + { + try + { + return LibraryManager.GetStudio(i); + } + catch + { + return null; + } + }).Where(i => i != null).Select(i => i.Id).ToArray(); + } + + foreach (var filter in request.GetFilters()) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + var result = GetItems(request, query); + + var dtos = result.Items.Select(i => + { + var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes)) + { + SetItemCounts(dto, i.Item2); + } + + return dto; + }); + + return new QueryResult<BaseItemDto> + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + protected virtual QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return new QueryResult<(BaseItem, ItemCounts)>(); + } + + private void SetItemCounts(BaseItemDto dto, ItemCounts counts) + { + dto.ChildCount = counts.ItemCount; + dto.ProgramCount = counts.ProgramCount; + dto.SeriesCount = counts.SeriesCount; + dto.EpisodeCount = counts.EpisodeCount; + dto.MovieCount = counts.MovieCount; + dto.TrailerCount = counts.TrailerCount; + dto.AlbumCount = counts.AlbumCount; + dto.SongCount = counts.SongCount; + dto.ArtistCount = counts.ArtistCount; + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{ItemsResult}.</returns> + protected QueryResult<BaseItemDto> GetResult(GetItemsByName request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + User user = null; + BaseItem parentItem; + + if (!request.UserId.Equals(Guid.Empty)) + { + user = UserManager.GetUserById(request.UserId); + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId); + } + else + { + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId); + } + + IList<BaseItem> items; + + var excludeItemTypes = request.GetExcludeItemTypes(); + var includeItemTypes = request.GetIncludeItemTypes(); + var mediaTypes = request.GetMediaTypes(); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, + DtoOptions = dtoOptions + }; + + bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); + + if (parentItem.IsFolder) + { + var folder = (Folder)parentItem; + + if (!request.UserId.Equals(Guid.Empty)) + { + items = request.Recursive ? + folder.GetRecursiveChildren(user, query).ToList() : + folder.GetChildren(user, true).Where(Filter).ToList(); + } + else + { + items = request.Recursive ? + folder.GetRecursiveChildren(Filter) : + folder.Children.Where(Filter).ToList(); + } + } + else + { + items = new[] { parentItem }.Where(Filter).ToList(); + } + + var extractedItems = GetAllItems(request, items); + + var filteredItems = LibraryManager.Sort(extractedItems, user, request.GetOrderBy()); + + var ibnItemsArray = filteredItems.ToList(); + + IEnumerable<BaseItem> ibnItems = ibnItemsArray; + + var result = new QueryResult<BaseItemDto> + { + TotalRecordCount = ibnItemsArray.Count + }; + + if (request.StartIndex.HasValue || request.Limit.HasValue) + { + if (request.StartIndex.HasValue) + { + ibnItems = ibnItems.Skip(request.StartIndex.Value); + } + + if (request.Limit.HasValue) + { + ibnItems = ibnItems.Take(request.Limit.Value); + } + } + + var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>())); + + var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); + + result.Items = dtos.Where(i => i != null).ToArray(); + + return result; + } + + /// <summary> + /// Filters the items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="f">The f.</param> + /// <param name="excludeItemTypes">The exclude item types.</param> + /// <param name="includeItemTypes">The include item types.</param> + /// <param name="mediaTypes">The media types.</param> + /// <returns>IEnumerable{BaseItem}.</returns> + private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes) + { + // Exclude item types + if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Include item types + if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Include MediaTypes + if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Task{`0}}.</returns> + protected abstract IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items); + } + + /// <summary> + /// Class GetItemsByName. + /// </summary> + public class GetItemsByName : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>> + { + public GetItemsByName() + { + Recursive = true; + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs new file mode 100644 index 000000000..344861a49 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -0,0 +1,478 @@ +using System; +using System.Linq; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Api.UserLibrary +{ + public abstract class BaseItemsRequest : IHasDtoOptions + { + protected BaseItemsRequest() + { + EnableImages = true; + EnableTotalRecordCount = true; + } + + /// <summary> + /// Gets or sets the max offical rating. + /// </summary> + /// <value>The max offical rating.</value> + [ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MaxOfficialRating { get; set; } + + [ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasThemeSong { get; set; } + + [ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasThemeVideo { get; set; } + + [ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasSubtitles { get; set; } + + [ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasSpecialFeature { get; set; } + + [ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasTrailer { get; set; } + + [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string AdjacentTo { get; set; } + + [ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MinIndexNumber { get; set; } + + [ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ParentIndexNumber { get; set; } + + [ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasParentalRating { get; set; } + + [ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsHD { get; set; } + + public bool? Is4K { get; set; } + + [ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string LocationTypes { get; set; } + + [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ExcludeLocationTypes { get; set; } + + [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsMissing { get; set; } + + [ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsUnaired { get; set; } + + [ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public double? MinCommunityRating { get; set; } + + [ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public double? MinCriticRating { get; set; } + + [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AiredDuringSeason { get; set; } + + [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MinPremiereDate { get; set; } + + [ApiMember(Name = "MinDateLastSaved", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MinDateLastSaved { get; set; } + + [ApiMember(Name = "MinDateLastSavedForUser", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MinDateLastSavedForUser { get; set; } + + [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MaxPremiereDate { get; set; } + + [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasOverview { get; set; } + + [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasImdbId { get; set; } + + [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasTmdbId { get; set; } + + [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasTvdbId { get; set; } + + [ApiMember(Name = "ExcludeItemIds", Description = "Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ExcludeItemIds { get; set; } + + public bool EnableTotalRecordCount { get; set; } + + /// <summary> + /// Skips over a given number of items within the results. Use for paging. + /// </summary> + /// <value>The start index.</value> + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? StartIndex { get; set; } + + /// <summary> + /// The maximum number of items to return. + /// </summary> + /// <value>The limit.</value> + [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Limit { get; set; } + + /// <summary> + /// Whether or not to perform the query recursively. + /// </summary> + /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value> + [ApiMember(Name = "Recursive", Description = "When searching within folders, this determines whether or not the search will be recursive. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool Recursive { get; set; } + + public string SearchTerm { get; set; } + + /// <summary> + /// Gets or sets the sort order. + /// </summary> + /// <value>The sort order.</value> + [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string SortOrder { get; set; } + + /// <summary> + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// </summary> + /// <value>The parent id.</value> + [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ParentId { get; set; } + + /// <summary> + /// Fields to return within the items, in addition to basic information. + /// </summary> + /// <value>The fields.</value> + [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Fields { get; set; } + + /// <summary> + /// Gets or sets the exclude item types. + /// </summary> + /// <value>The exclude item types.</value> + [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ExcludeItemTypes { get; set; } + + /// <summary> + /// Gets or sets the include item types. + /// </summary> + /// <value>The include item types.</value> + [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string IncludeItemTypes { get; set; } + + /// <summary> + /// Filters to apply to the results. + /// </summary> + /// <value>The filters.</value> + [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Filters { get; set; } + + /// <summary> + /// Gets or sets the Isfavorite option. + /// </summary> + /// <value>IsFavorite</value> + [ApiMember(Name = "IsFavorite", Description = "Optional filter by items that are marked as favorite, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsFavorite { get; set; } + + /// <summary> + /// Gets or sets the media types. + /// </summary> + /// <value>The media types.</value> + [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string MediaTypes { get; set; } + + /// <summary> + /// Gets or sets the image types. + /// </summary> + /// <value>The image types.</value> + [ApiMember(Name = "ImageTypes", Description = "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ImageTypes { get; set; } + + /// <summary> + /// What to sort the results by. + /// </summary> + /// <value>The sort by.</value> + [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string SortBy { get; set; } + + [ApiMember(Name = "IsPlayed", Description = "Optional filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsPlayed { get; set; } + + /// <summary> + /// Limit results to items containing specific genres. + /// </summary> + /// <value>The genres.</value> + [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Genres { get; set; } + + public string GenreIds { get; set; } + + [ApiMember(Name = "OfficialRatings", Description = "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string OfficialRatings { get; set; } + + [ApiMember(Name = "Tags", Description = "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Tags { get; set; } + + /// <summary> + /// Limit results to items containing specific years. + /// </summary> + /// <value>The years.</value> + [ApiMember(Name = "Years", Description = "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Years { get; set; } + + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } + + /// <summary> + /// Limit results to items containing a specific person. + /// </summary> + /// <value>The person.</value> + [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Person { get; set; } + + [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string PersonIds { get; set; } + + /// <summary> + /// If the Person filter is used, this can also be used to restrict to a specific person type. + /// </summary> + /// <value>The type of the person.</value> + [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string PersonTypes { get; set; } + + /// <summary> + /// Limit results to items containing specific studios. + /// </summary> + /// <value>The studios.</value> + [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Studios { get; set; } + + [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string StudioIds { get; set; } + + /// <summary> + /// Gets or sets the studios. + /// </summary> + /// <value>The studios.</value> + [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Artists { get; set; } + + public string ExcludeArtistIds { get; set; } + + [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ArtistIds { get; set; } + + public string AlbumArtistIds { get; set; } + + public string ContributingArtistIds { get; set; } + + [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Albums { get; set; } + + public string AlbumIds { get; set; } + + /// <summary> + /// Gets or sets the item ids. + /// </summary> + /// <value>The item ids.</value> + [ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Ids { get; set; } + + /// <summary> + /// Gets or sets the video types. + /// </summary> + /// <value>The video types.</value> + [ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string VideoTypes { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the min offical rating. + /// </summary> + /// <value>The min offical rating.</value> + [ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MinOfficialRating { get; set; } + + [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? IsLocked { get; set; } + + [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? IsPlaceHolder { get; set; } + + [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasOfficialRating { get; set; } + + [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? CollapseBoxSetItems { get; set; } + + public int? MinWidth { get; set; } + + public int? MinHeight { get; set; } + + public int? MaxWidth { get; set; } + + public int? MaxHeight { get; set; } + + /// <summary> + /// Gets or sets the video formats. + /// </summary> + /// <value>The video formats.</value> + [ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? Is3D { get; set; } + + /// <summary> + /// Gets or sets the series status. + /// </summary> + /// <value>The series status.</value> + [ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string SeriesStatus { get; set; } + + [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameStartsWithOrGreater { get; set; } + + [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameStartsWith { get; set; } + + [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameLessThan { get; set; } + + public string[] GetGenres() + { + return (Genres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetTags() + { + return (Tags ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetOfficialRatings() + { + return (OfficialRatings ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetMediaTypes() + { + return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetIncludeItemTypes() + { + return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetExcludeItemTypes() + { + return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public int[] GetYears() + { + return (Years ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); + } + + public string[] GetStudios() + { + return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetPersonTypes() + { + return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public VideoType[] GetVideoTypes() + { + return string.IsNullOrEmpty(VideoTypes) + ? Array.Empty<VideoType>() + : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse<VideoType>(v, true)).ToArray(); + } + + /// <summary> + /// Gets the filters. + /// </summary> + /// <returns>IEnumerable{ItemFilter}.</returns> + public ItemFilter[] GetFilters() + { + var val = Filters; + + return string.IsNullOrEmpty(val) + ? Array.Empty<ItemFilter>() + : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray(); + } + + /// <summary> + /// Gets the image types. + /// </summary> + /// <returns>IEnumerable{ImageType}.</returns> + public ImageType[] GetImageTypes() + { + var val = ImageTypes; + + return string.IsNullOrEmpty(val) + ? Array.Empty<ImageType>() + : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray(); + } + + /// <summary> + /// Gets the order by. + /// </summary> + /// <returns>IEnumerable{ItemSortBy}.</returns> + public ValueTuple<string, SortOrder>[] GetOrderBy() + { + return GetOrderBy(SortBy, SortOrder); + } + + public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder) + { + var val = sortBy; + + if (string.IsNullOrEmpty(val)) + { + return Array.Empty<ValueTuple<string, SortOrder>>(); + } + + var vals = val.Split(','); + if (string.IsNullOrWhiteSpace(requestedSortOrder)) + { + requestedSortOrder = "Ascending"; + } + + var sortOrders = requestedSortOrder.Split(','); + + var result = new ValueTuple<string, SortOrder>[vals.Length]; + + for (var i = 0; i < vals.Length; i++) + { + var sortOrderIndex = sortOrders.Length > i ? i : 0; + + var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null; + var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) + ? MediaBrowser.Model.Entities.SortOrder.Descending + : MediaBrowser.Model.Entities.SortOrder.Ascending; + + result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder); + } + + return result; + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs new file mode 100644 index 000000000..7bdfbac98 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetGenres. + /// </summary> + [Route("/Genres", "GET", Summary = "Gets all genres from a given item, folder, or the entire library")] + public class GetGenres : GetItemsByName + { + } + + /// <summary> + /// Class GetGenre. + /// </summary> + [Route("/Genres/{Name}", "GET", Summary = "Gets a genre, by name")] + public class GetGenre : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class GenresService. + /// </summary> + [Authenticated] + public class GenresService : BaseItemsByNameService<Genre> + { + public GenresService( + ILogger<GenresService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + userDataRepository, + dtoService, + authorizationContext) + { + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetGenre request) + { + var result = GetItem(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the item. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{BaseItemDto}.</returns> + private BaseItemDto GetItem(GetGenre request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + var item = GetGenre(request.Name, LibraryManager, dtoOptions); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + + return DtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return DtoService.GetBaseItemDto(item, dtoOptions); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetGenres request) + { + var result = GetResultSlim(request); + + return ToOptimizedResult(result); + } + + protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) + { + var viewType = GetParentItemViewType(request); + + if (string.Equals(viewType, CollectionType.Music) || string.Equals(viewType, CollectionType.MusicVideos)) + { + return LibraryManager.GetMusicGenres(query); + } + + return LibraryManager.GetGenres(query); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items) + { + throw new NotImplementedException(); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs new file mode 100644 index 000000000..7efe0552c --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetItems. + /// </summary> + [Route("/Items", "GET", Summary = "Gets items based on a query.")] + [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")] + public class GetItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>> + { + } + + [Route("/Users/{UserId}/Items/Resume", "GET", Summary = "Gets items based on a query.")] + public class GetResumeItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>> + { + } + + /// <summary> + /// Class ItemsService. + /// </summary> + [Authenticated] + public class ItemsService : BaseApiService + { + /// <summary> + /// The _user manager. + /// </summary> + private readonly IUserManager _userManager; + + /// <summary> + /// The _library manager. + /// </summary> + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + + private readonly IDtoService _dtoService; + private readonly IAuthorizationContext _authContext; + + /// <summary> + /// Initializes a new instance of the <see cref="ItemsService" /> class. + /// </summary> + /// <param name="userManager">The user manager.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization.</param> + /// <param name="dtoService">The dto service.</param> + public ItemsService( + ILogger<ItemsService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + ILocalizationManager localization, + IDtoService dtoService, + IAuthorizationContext authContext) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _userManager = userManager; + _libraryManager = libraryManager; + _localization = localization; + _dtoService = dtoService; + _authContext = authContext; + } + + public object Get(GetResumeItems request) + { + var user = _userManager.GetUserById(request.UserId); + + var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId); + + var options = GetDtoOptions(_authContext, request); + + var ancestorIds = Array.Empty<Guid>(); + + var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes); + if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) + { + ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) + .Where(i => i is Folder) + .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) + .Select(i => i.Id) + .ToArray(); + } + + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, + IsResumable = true, + StartIndex = request.StartIndex, + Limit = request.Limit, + ParentId = parentIdGuid, + Recursive = true, + DtoOptions = options, + MediaTypes = request.GetMediaTypes(), + IsVirtualItem = false, + CollapseBoxSetItems = false, + EnableTotalRecordCount = request.EnableTotalRecordCount, + AncestorIds = ancestorIds, + IncludeItemTypes = request.GetIncludeItemTypes(), + ExcludeItemTypes = request.GetExcludeItemTypes(), + SearchTerm = request.SearchTerm + }); + + var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, options, user); + + var result = new QueryResult<BaseItemDto> + { + StartIndex = request.StartIndex.GetValueOrDefault(), + TotalRecordCount = itemsResult.TotalRecordCount, + Items = returnItems + }; + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetItems request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var result = GetItems(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="request">The request.</param> + private QueryResult<BaseItemDto> GetItems(GetItems request) + { + var user = request.UserId == Guid.Empty ? null : _userManager.GetUserById(request.UserId); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var result = GetQueryResult(request, dtoOptions, user); + + if (result == null) + { + throw new InvalidOperationException("GetItemsToSerialize returned null"); + } + + if (result.Items == null) + { + throw new InvalidOperationException("GetItemsToSerialize result.Items returned null"); + } + + var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); + + return new QueryResult<BaseItemDto> + { + StartIndex = request.StartIndex.GetValueOrDefault(), + TotalRecordCount = result.TotalRecordCount, + Items = dtoList + }; + } + + /// <summary> + /// Gets the items to serialize. + /// </summary> + private QueryResult<BaseItem> GetQueryResult(GetItems request, DtoOptions dtoOptions, User user) + { + if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) + || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) + { + request.ParentId = null; + } + + BaseItem item = null; + + if (!string.IsNullOrEmpty(request.ParentId)) + { + item = _libraryManager.GetItemById(request.ParentId); + } + + if (item == null) + { + item = _libraryManager.GetUserRootFolder(); + } + + if (!(item is Folder folder)) + { + folder = _libraryManager.GetUserRootFolder(); + } + + if (folder is IHasCollectionType hasCollectionType + && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + { + request.Recursive = true; + request.IncludeItemTypes = "Playlist"; + } + + bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) + // Assume all folders inside an EnabledChannel are enabled + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id); + + var collectionFolders = _libraryManager.GetCollectionFolders(item); + foreach (var collectionFolder in collectionFolders) + { + if (user.GetPreference(PreferenceKind.EnabledFolders).Contains( + collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), + StringComparer.OrdinalIgnoreCase)) + { + isInEnabledFolder = true; + } + } + + if (!(item is UserRootFolder) + && !isInEnabledFolder + && !user.HasPermission(PermissionKind.EnableAllFolders) + && !user.HasPermission(PermissionKind.EnableAllChannels)) + { + Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); + return new QueryResult<BaseItem> + { + Items = Array.Empty<BaseItem>(), + TotalRecordCount = 0, + StartIndex = 0 + }; + } + + if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder)) + { + return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); + } + + var itemsArray = folder.GetChildren(user, true); + return new QueryResult<BaseItem> + { + Items = itemsArray, + TotalRecordCount = itemsArray.Count, + StartIndex = 0 + }; + } + + private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, User user) + { + var query = new InternalItemsQuery(user) + { + IsPlayed = request.IsPlayed, + MediaTypes = request.GetMediaTypes(), + IncludeItemTypes = request.GetIncludeItemTypes(), + ExcludeItemTypes = request.GetExcludeItemTypes(), + Recursive = request.Recursive, + OrderBy = request.GetOrderBy(), + + IsFavorite = request.IsFavorite, + Limit = request.Limit, + StartIndex = request.StartIndex, + IsMissing = request.IsMissing, + IsUnaired = request.IsUnaired, + CollapseBoxSetItems = request.CollapseBoxSetItems, + NameLessThan = request.NameLessThan, + NameStartsWith = request.NameStartsWith, + NameStartsWithOrGreater = request.NameStartsWithOrGreater, + HasImdbId = request.HasImdbId, + IsPlaceHolder = request.IsPlaceHolder, + IsLocked = request.IsLocked, + MinWidth = request.MinWidth, + MinHeight = request.MinHeight, + MaxWidth = request.MaxWidth, + MaxHeight = request.MaxHeight, + Is3D = request.Is3D, + HasTvdbId = request.HasTvdbId, + HasTmdbId = request.HasTmdbId, + HasOverview = request.HasOverview, + HasOfficialRating = request.HasOfficialRating, + HasParentalRating = request.HasParentalRating, + HasSpecialFeature = request.HasSpecialFeature, + HasSubtitles = request.HasSubtitles, + HasThemeSong = request.HasThemeSong, + HasThemeVideo = request.HasThemeVideo, + HasTrailer = request.HasTrailer, + IsHD = request.IsHD, + Is4K = request.Is4K, + Tags = request.GetTags(), + OfficialRatings = request.GetOfficialRatings(), + Genres = request.GetGenres(), + ArtistIds = GetGuids(request.ArtistIds), + AlbumArtistIds = GetGuids(request.AlbumArtistIds), + ContributingArtistIds = GetGuids(request.ContributingArtistIds), + GenreIds = GetGuids(request.GenreIds), + StudioIds = GetGuids(request.StudioIds), + Person = request.Person, + PersonIds = GetGuids(request.PersonIds), + PersonTypes = request.GetPersonTypes(), + Years = request.GetYears(), + ImageTypes = request.GetImageTypes(), + VideoTypes = request.GetVideoTypes(), + AdjacentTo = request.AdjacentTo, + ItemIds = GetGuids(request.Ids), + MinCommunityRating = request.MinCommunityRating, + MinCriticRating = request.MinCriticRating, + ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId), + ParentIndexNumber = request.ParentIndexNumber, + EnableTotalRecordCount = request.EnableTotalRecordCount, + ExcludeItemIds = GetGuids(request.ExcludeItemIds), + DtoOptions = dtoOptions, + SearchTerm = request.SearchTerm + }; + + if (!string.IsNullOrWhiteSpace(request.Ids) || !string.IsNullOrWhiteSpace(request.SearchTerm)) + { + query.CollapseBoxSetItems = false; + } + + foreach (var filter in request.GetFilters()) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + if (!string.IsNullOrEmpty(request.MinDateLastSaved)) + { + query.MinDateLastSaved = DateTime.Parse(request.MinDateLastSaved, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + } + + if (!string.IsNullOrEmpty(request.MinDateLastSavedForUser)) + { + query.MinDateLastSavedForUser = DateTime.Parse(request.MinDateLastSavedForUser, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + } + + if (!string.IsNullOrEmpty(request.MinPremiereDate)) + { + query.MinPremiereDate = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + } + + if (!string.IsNullOrEmpty(request.MaxPremiereDate)) + { + query.MaxPremiereDate = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + } + + // Filter by Series Status + if (!string.IsNullOrEmpty(request.SeriesStatus)) + { + query.SeriesStatuses = request.SeriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray(); + } + + // ExcludeLocationTypes + if (!string.IsNullOrEmpty(request.ExcludeLocationTypes)) + { + var excludeLocationTypes = request.ExcludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray(); + if (excludeLocationTypes.Contains(LocationType.Virtual)) + { + query.IsVirtualItem = false; + } + } + + if (!string.IsNullOrEmpty(request.LocationTypes)) + { + var requestedLocationTypes = + request.LocationTypes.Split(','); + + if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4) + { + query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString()); + } + } + + // Min official rating + if (!string.IsNullOrWhiteSpace(request.MinOfficialRating)) + { + query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating); + } + + // Max official rating + if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating)) + { + query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating); + } + + // Artists + if (!string.IsNullOrEmpty(request.Artists)) + { + query.ArtistIds = request.Artists.Split('|').Select(i => + { + try + { + return _libraryManager.GetArtist(i, new DtoOptions(false)); + } + catch + { + return null; + } + }).Where(i => i != null).Select(i => i.Id).ToArray(); + } + + // ExcludeArtistIds + if (!string.IsNullOrWhiteSpace(request.ExcludeArtistIds)) + { + query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds); + } + + if (!string.IsNullOrWhiteSpace(request.AlbumIds)) + { + query.AlbumIds = GetGuids(request.AlbumIds); + } + + // Albums + if (!string.IsNullOrEmpty(request.Albums)) + { + query.AlbumIds = request.Albums.Split('|').SelectMany(i => + { + return _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + Name = i, + Limit = 1 + }); + }).ToArray(); + } + + // Studios + if (!string.IsNullOrEmpty(request.Studios)) + { + query.StudioIds = request.Studios.Split('|').Select(i => + { + try + { + return _libraryManager.GetStudio(i); + } + catch + { + return null; + } + }).Where(i => i != null).Select(i => i.Id).ToArray(); + } + + // Apply default sorting if none requested + if (query.OrderBy.Count == 0) + { + // Albums by artist + if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase)) + { + query.OrderBy = new[] + { + new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), + new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) + }; + } + } + + return query; + } + } + + /// <summary> + /// Class DateCreatedComparer. + /// </summary> + public class DateCreatedComparer : IComparer<BaseItem> + { + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem x, BaseItem y) + { + return x.DateCreated.CompareTo(y.DateCreated); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs new file mode 100644 index 000000000..7924339ed --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/PersonsService.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetPersons. + /// </summary> + [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")] + public class GetPersons : GetItemsByName + { + } + + /// <summary> + /// Class GetPerson. + /// </summary> + [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")] + public class GetPerson : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class PersonsService. + /// </summary> + [Authenticated] + public class PersonsService : BaseItemsByNameService<Person> + { + public PersonsService( + ILogger<PersonsService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + userDataRepository, + dtoService, + authorizationContext) + { + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetPerson request) + { + var result = GetItem(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the item. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{BaseItemDto}.</returns> + private BaseItemDto GetItem(GetPerson request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + var item = GetPerson(request.Name, LibraryManager, dtoOptions); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + + return DtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return DtoService.GetBaseItemDto(item, dtoOptions); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetPersons request) + { + return GetResultSlim(request); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items) + { + throw new NotImplementedException(); + } + + protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) + { + var items = LibraryManager.GetPeopleItems(new InternalPeopleQuery + { + PersonTypes = query.PersonTypes, + NameContains = query.NameContains ?? query.SearchTerm + }); + + if ((query.IsFavorite ?? false) && query.User != null) + { + items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList(); + } + + return new QueryResult<(BaseItem, ItemCounts)> + { + TotalRecordCount = items.Count, + Items = items.Take(query.Limit ?? int.MaxValue).Select(i => (i as BaseItem, new ItemCounts())).ToArray() + }; + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs new file mode 100644 index 000000000..d809cc2e7 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -0,0 +1,456 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Session; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class MarkPlayedItem. + /// </summary> + [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")] + public class MarkPlayedItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string DatePlayed { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + } + + /// <summary> + /// Class MarkUnplayedItem. + /// </summary> + [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")] + public class MarkUnplayedItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + } + + [Route("/Sessions/Playing", "POST", Summary = "Reports playback has started within a session")] + public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid + { + } + + [Route("/Sessions/Playing/Progress", "POST", Summary = "Reports playback progress within a session")] + public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid + { + } + + [Route("/Sessions/Playing/Ping", "POST", Summary = "Pings a playback session")] + public class PingPlaybackSession : IReturnVoid + { + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + [Route("/Sessions/Playing/Stopped", "POST", Summary = "Reports playback has stopped within a session")] + public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid + { + } + + /// <summary> + /// Class OnPlaybackStart. + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")] + public class OnPlaybackStart : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MediaSourceId { get; set; } + + [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool CanSeek { get; set; } + + [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? AudioStreamIndex { get; set; } + + [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + /// <summary> + /// Class OnPlaybackProgress. + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")] + public class OnPlaybackProgress : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MediaSourceId { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public long? PositionTicks { get; set; } + + [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool IsPaused { get; set; } + + [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool IsMuted { get; set; } + + [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? AudioStreamIndex { get; set; } + + [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? VolumeLevel { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + + [ApiMember(Name = "RepeatMode", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public RepeatMode RepeatMode { get; set; } + } + + /// <summary> + /// Class OnPlaybackStopped. + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")] + public class OnPlaybackStopped : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string MediaSourceId { get; set; } + + [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string NextMediaType { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")] + public long? PositionTicks { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + [Authenticated] + public class PlaystateService : BaseApiService + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; + private readonly ISessionContext _sessionContext; + private readonly IAuthorizationContext _authContext; + + public PlaystateService( + ILogger<PlaystateService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + IUserDataManager userDataRepository, + ILibraryManager libraryManager, + ISessionManager sessionManager, + ISessionContext sessionContext, + IAuthorizationContext authContext) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _sessionManager = sessionManager; + _sessionContext = sessionContext; + _authContext = authContext; + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Post(MarkPlayedItem request) + { + var result = MarkPlayed(request); + + return ToOptimizedResult(result); + } + + private UserItemDataDto MarkPlayed(MarkPlayedItem request) + { + var user = _userManager.GetUserById(Guid.Parse(request.UserId)); + + DateTime? datePlayed = null; + + if (!string.IsNullOrEmpty(request.DatePlayed)) + { + datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + } + + var session = GetSession(_sessionContext); + + var dto = UpdatePlayedStatus(user, request.Id, true, datePlayed); + + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + + UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed); + } + + return dto; + } + + private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId) + { + if (method == PlayMethod.Transcode) + { + var job = string.IsNullOrWhiteSpace(playSessionId) ? null : ApiEntryPoint.Instance.GetTranscodingJob(playSessionId); + if (job == null) + { + return PlayMethod.DirectPlay; + } + } + + return method; + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(OnPlaybackStart request) + { + Post(new ReportPlaybackStart + { + CanSeek = request.CanSeek, + ItemId = new Guid(request.Id), + MediaSourceId = request.MediaSourceId, + AudioStreamIndex = request.AudioStreamIndex, + SubtitleStreamIndex = request.SubtitleStreamIndex, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId + }); + } + + public void Post(ReportPlaybackStart request) + { + request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); + + request.SessionId = GetSession(_sessionContext).Id; + + var task = _sessionManager.OnPlaybackStart(request); + + Task.WaitAll(task); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(OnPlaybackProgress request) + { + Post(new ReportPlaybackProgress + { + ItemId = new Guid(request.Id), + PositionTicks = request.PositionTicks, + IsMuted = request.IsMuted, + IsPaused = request.IsPaused, + MediaSourceId = request.MediaSourceId, + AudioStreamIndex = request.AudioStreamIndex, + SubtitleStreamIndex = request.SubtitleStreamIndex, + VolumeLevel = request.VolumeLevel, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId, + RepeatMode = request.RepeatMode + }); + } + + public void Post(ReportPlaybackProgress request) + { + request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); + + request.SessionId = GetSession(_sessionContext).Id; + + var task = _sessionManager.OnPlaybackProgress(request); + + Task.WaitAll(task); + } + + public void Post(PingPlaybackSession request) + { + ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId, null); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public Task Delete(OnPlaybackStopped request) + { + return Post(new ReportPlaybackStopped + { + ItemId = new Guid(request.Id), + PositionTicks = request.PositionTicks, + MediaSourceId = request.MediaSourceId, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId, + NextMediaType = request.NextMediaType + }); + } + + public async Task Post(ReportPlaybackStopped request) + { + Logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", request.PlaySessionId ?? string.Empty); + + if (!string.IsNullOrWhiteSpace(request.PlaySessionId)) + { + await ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true); + } + + request.SessionId = GetSession(_sessionContext).Id; + + await _sessionManager.OnPlaybackStopped(request); + } + + /// <summary> + /// Deletes the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Delete(MarkUnplayedItem request) + { + var task = MarkUnplayed(request); + + return ToOptimizedResult(task); + } + + private UserItemDataDto MarkUnplayed(MarkUnplayedItem request) + { + var user = _userManager.GetUserById(Guid.Parse(request.UserId)); + + var session = GetSession(_sessionContext); + + var dto = UpdatePlayedStatus(user, request.Id, false, null); + + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + + UpdatePlayedStatus(additionalUser, request.Id, false, null); + } + + return dto; + } + + /// <summary> + /// Updates the played status. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="itemId">The item id.</param> + /// <param name="wasPlayed">if set to <c>true</c> [was played].</param> + /// <param name="datePlayed">The date played.</param> + /// <returns>Task.</returns> + private UserItemDataDto UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) + { + var item = _libraryManager.GetItemById(itemId); + + if (wasPlayed) + { + item.MarkPlayed(user, datePlayed, true); + } + else + { + item.MarkUnplayed(user); + } + + return _userDataRepository.GetUserDataDto(item, user); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs new file mode 100644 index 000000000..66350955f --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/StudiosService.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetStudios. + /// </summary> + [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")] + public class GetStudios : GetItemsByName + { + } + + /// <summary> + /// Class GetStudio. + /// </summary> + [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")] + public class GetStudio : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class StudiosService. + /// </summary> + [Authenticated] + public class StudiosService : BaseItemsByNameService<Studio> + { + public StudiosService( + ILogger<StudiosService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + userDataRepository, + dtoService, + authorizationContext) + { + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetStudio request) + { + var result = GetItem(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the item. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{BaseItemDto}.</returns> + private BaseItemDto GetItem(GetStudio request) + { + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + var item = GetStudio(request.Name, LibraryManager, dtoOptions); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + + return DtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return DtoService.GetBaseItemDto(item, dtoOptions); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetStudios request) + { + var result = GetResultSlim(request); + + return ToOptimizedResult(result); + } + + protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return LibraryManager.GetStudios(query); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items) + { + throw new NotImplementedException(); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs new file mode 100644 index 000000000..f9cbba410 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -0,0 +1,575 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetItem. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")] + public class GetItem : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + /// <summary> + /// Class GetItem. + /// </summary> + [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")] + public class GetRootFolder : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class GetIntros. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")] + public class GetIntros : IReturn<QueryResult<BaseItemDto>> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + /// <summary> + /// Class MarkFavoriteItem. + /// </summary> + [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")] + public class MarkFavoriteItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid Id { get; set; } + } + + /// <summary> + /// Class UnmarkFavoriteItem. + /// </summary> + [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")] + public class UnmarkFavoriteItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid Id { get; set; } + } + + /// <summary> + /// Class ClearUserItemRating. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")] + public class DeleteUserItemRating : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid Id { get; set; } + } + + /// <summary> + /// Class UpdateUserItemRating. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")] + public class UpdateUserItemRating : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid Id { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes. + /// </summary> + /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value> + [ApiMember(Name = "Likes", Description = "Whether the user likes the item or not. true/false", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool Likes { get; set; } + } + + /// <summary> + /// Class GetLocalTrailers. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")] + public class GetLocalTrailers : IReturn<BaseItemDto[]> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + /// <summary> + /// Class GetSpecialFeatures. + /// </summary> + [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")] + public class GetSpecialFeatures : IReturn<BaseItemDto[]> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")] + public class GetLatestMedia : IReturn<BaseItemDto[]>, IHasDtoOptions + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int Limit { get; set; } + + [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid ParentId { get; set; } + + [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Fields { get; set; } + + [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string IncludeItemTypes { get; set; } + + [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsFolder { get; set; } + + [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsPlayed { get; set; } + + [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool GroupItems { get; set; } + + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + + public GetLatestMedia() + { + Limit = 20; + GroupItems = true; + } + } + + /// <summary> + /// Class UserLibraryService. + /// </summary> + [Authenticated] + public class UserLibraryService : BaseApiService + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserViewManager _userViewManager; + private readonly IFileSystem _fileSystem; + private readonly IAuthorizationContext _authContext; + + public UserLibraryService( + ILogger<UserLibraryService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IUserViewManager userViewManager, + IFileSystem fileSystem, + IAuthorizationContext authContext) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _userManager = userManager; + _libraryManager = libraryManager; + _userDataRepository = userDataRepository; + _dtoService = dtoService; + _userViewManager = userViewManager; + _fileSystem = fileSystem; + _authContext = authContext; + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetSpecialFeatures request) + { + var result = GetAsync(request); + + return ToOptimizedResult(result); + } + + public object Get(GetLatestMedia request) + { + var user = _userManager.GetUserById(request.UserId); + + if (!request.IsPlayed.HasValue) + { + if (user.HidePlayedInLatest) + { + request.IsPlayed = false; + } + } + + var dtoOptions = GetDtoOptions(_authContext, request); + + var list = _userViewManager.GetLatestItems(new LatestItemsQuery + { + GroupItems = request.GroupItems, + IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true), + IsPlayed = request.IsPlayed, + Limit = request.Limit, + ParentId = request.ParentId, + UserId = request.UserId, + }, dtoOptions); + + var dtos = list.Select(i => + { + var item = i.Item2[0]; + var childCount = 0; + + if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum)) + { + item = i.Item1; + childCount = i.Item2.Count; + } + + var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user); + + dto.ChildCount = childCount; + + return dto; + }); + + return ToOptimizedResult(dtos.ToArray()); + } + + private BaseItemDto[] GetAsync(GetSpecialFeatures request) + { + var user = _userManager.GetUserById(request.UserId); + + var item = string.IsNullOrEmpty(request.Id) ? + _libraryManager.GetUserRootFolder() : + _libraryManager.GetItemById(request.Id); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var dtos = item + .GetExtras(BaseItem.DisplayExtraTypes) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); + + return dtos.ToArray(); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetLocalTrailers request) + { + var user = _userManager.GetUserById(request.UserId); + + var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer }) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); + + if (item is IHasTrailers hasTrailers) + { + var trailers = hasTrailers.GetTrailers(); + var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); + var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; + dtosExtras.CopyTo(allTrailers, 0); + dtosTrailers.CopyTo(allTrailers, dtosExtras.Length); + return ToOptimizedResult(allTrailers); + } + + return ToOptimizedResult(dtosExtras); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public async Task<object> Get(GetItem request) + { + var user = _userManager.GetUserById(request.UserId); + + var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id); + + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var result = _dtoService.GetBaseItemDto(item, dtoOptions, user); + + return ToOptimizedResult(result); + } + + private async Task RefreshItemOnDemandIfNeeded(BaseItem item) + { + if (item is Person) + { + var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); + var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; + + if (!hasMetdata) + { + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = performFullRefresh + }; + + await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); + } + } + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetRootFolder request) + { + var user = _userManager.GetUserById(request.UserId); + + var item = _libraryManager.GetUserRootFolder(); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var result = _dtoService.GetBaseItemDto(item, dtoOptions, user); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public async Task<object> Get(GetIntros request) + { + var user = _userManager.GetUserById(request.UserId); + + var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id); + + var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); + + var dtoOptions = GetDtoOptions(_authContext, request); + + var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); + + var result = new QueryResult<BaseItemDto> + { + Items = dtos, + TotalRecordCount = dtos.Length + }; + + return ToOptimizedResult(result); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Post(MarkFavoriteItem request) + { + var dto = MarkFavorite(request.UserId, request.Id, true); + + return ToOptimizedResult(dto); + } + + /// <summary> + /// Deletes the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Delete(UnmarkFavoriteItem request) + { + var dto = MarkFavorite(request.UserId, request.Id, false); + + return ToOptimizedResult(dto); + } + + /// <summary> + /// Marks the favorite. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param> + private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + + // Get the user data for this item + var data = _userDataRepository.GetUserData(user, item); + + // Set favorite status + data.IsFavorite = isFavorite; + + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + + return _userDataRepository.GetUserDataDto(item, user); + } + + /// <summary> + /// Deletes the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Delete(DeleteUserItemRating request) + { + var dto = UpdateUserItemRating(request.UserId, request.Id, null); + + return ToOptimizedResult(dto); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Post(UpdateUserItemRating request) + { + var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes); + + return ToOptimizedResult(dto); + } + + /// <summary> + /// Updates the user item rating. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="likes">if set to <c>true</c> [likes].</param> + private UserItemDataDto UpdateUserItemRating(Guid userId, Guid itemId, bool? likes) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + + // Get the user data for this item + var data = _userDataRepository.GetUserData(user, item); + + data.Likes = likes; + + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + + return _userDataRepository.GetUserDataDto(item, user); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs new file mode 100644 index 000000000..6f1620ddd --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/UserViewsService.cs @@ -0,0 +1,148 @@ +using System; +using System.Globalization; +using System.Linq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + [Route("/Users/{UserId}/Views", "GET")] + public class GetUserViews : IReturn<QueryResult<BaseItemDto>> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + + [ApiMember(Name = "IncludeExternalContent", Description = "Whether or not to include external views such as channels or live tv", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? IncludeExternalContent { get; set; } + + public bool IncludeHidden { get; set; } + + public string PresetViews { get; set; } + } + + [Route("/Users/{UserId}/GroupingOptions", "GET")] + public class GetGroupingOptions : IReturn<SpecialViewOption[]> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid UserId { get; set; } + } + + public class UserViewsService : BaseApiService + { + private readonly IUserManager _userManager; + private readonly IUserViewManager _userViewManager; + private readonly IDtoService _dtoService; + private readonly IAuthorizationContext _authContext; + private readonly ILibraryManager _libraryManager; + + public UserViewsService( + ILogger<UserViewsService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + IUserViewManager userViewManager, + IDtoService dtoService, + IAuthorizationContext authContext, + ILibraryManager libraryManager) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _userManager = userManager; + _userViewManager = userViewManager; + _dtoService = dtoService; + _authContext = authContext; + _libraryManager = libraryManager; + } + + public object Get(GetUserViews request) + { + var query = new UserViewQuery + { + UserId = request.UserId + }; + + if (request.IncludeExternalContent.HasValue) + { + query.IncludeExternalContent = request.IncludeExternalContent.Value; + } + + query.IncludeHidden = request.IncludeHidden; + + if (!string.IsNullOrWhiteSpace(request.PresetViews)) + { + query.PresetViews = request.PresetViews.Split(','); + } + + var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; + if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) + { + query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }; + } + + var folders = _userViewManager.GetUserViews(query); + + var dtoOptions = GetDtoOptions(_authContext, request); + var fields = dtoOptions.Fields.ToList(); + + fields.Add(ItemFields.PrimaryImageAspectRatio); + fields.Add(ItemFields.DisplayPreferencesId); + fields.Remove(ItemFields.BasicSyncInfo); + dtoOptions.Fields = fields.ToArray(); + + var user = _userManager.GetUserById(request.UserId); + + var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) + .ToArray(); + + var result = new QueryResult<BaseItemDto> + { + Items = dtos, + TotalRecordCount = dtos.Length + }; + + return ToOptimizedResult(result); + } + + public object Get(GetGroupingOptions request) + { + var user = _userManager.GetUserById(request.UserId); + + var list = _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .Where(UserView.IsEligibleForGrouping) + .Select(i => new SpecialViewOption + { + Name = i.Name, + Id = i.Id.ToString("N", CultureInfo.InvariantCulture) + }) + .OrderBy(i => i.Name) + .ToArray(); + + return ToOptimizedResult(list); + } + } + + class SpecialViewOption + { + public string Name { get; set; } + + public string Id { get; set; } + } +} diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs new file mode 100644 index 000000000..0523f89fa --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/YearsService.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class GetYears. + /// </summary> + [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")] + public class GetYears : GetItemsByName + { + } + + /// <summary> + /// Class GetYear. + /// </summary> + [Route("/Years/{Year}", "GET", Summary = "Gets a year")] + public class GetYear : IReturn<BaseItemDto> + { + /// <summary> + /// Gets or sets the year. + /// </summary> + /// <value>The year.</value> + [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] + public int Year { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + /// <summary> + /// Class YearsService. + /// </summary> + [Authenticated] + public class YearsService : BaseItemsByNameService<Year> + { + public YearsService( + ILogger<YearsService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IUserDataManager userDataRepository, + IDtoService dtoService, + IAuthorizationContext authorizationContext) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + userDataRepository, + dtoService, + authorizationContext) + { + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetYear request) + { + var result = GetItem(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets the item. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{BaseItemDto}.</returns> + private BaseItemDto GetItem(GetYear request) + { + var item = LibraryManager.GetYear(request.Year); + + var dtoOptions = GetDtoOptions(AuthorizationContext, request); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = UserManager.GetUserById(request.UserId); + + return DtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return DtoService.GetBaseItemDto(item, dtoOptions); + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetYears request) + { + var result = GetResult(request); + + return ToOptimizedResult(result); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items) + { + return items + .Select(i => i.ProductionYear ?? 0) + .Where(i => i > 0) + .Distinct() + .Select(year => LibraryManager.GetYear(year)); + } + } +} |
