aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Api/UserLibrary
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Api/UserLibrary')
-rw-r--r--MediaBrowser.Api/UserLibrary/ArtistsService.cs143
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs388
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs478
-rw-r--r--MediaBrowser.Api/UserLibrary/GenresService.cs140
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs514
-rw-r--r--MediaBrowser.Api/UserLibrary/PersonsService.cs146
-rw-r--r--MediaBrowser.Api/UserLibrary/PlaystateService.cs456
-rw-r--r--MediaBrowser.Api/UserLibrary/StudiosService.cs132
-rw-r--r--MediaBrowser.Api/UserLibrary/UserLibraryService.cs575
-rw-r--r--MediaBrowser.Api/UserLibrary/UserViewsService.cs148
-rw-r--r--MediaBrowser.Api/UserLibrary/YearsService.cs131
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));
+ }
+ }
+}