From 60ab4cd7b12725d698edfdc07292d8d6e5cded5c Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 24 Jun 2020 12:23:46 -0600 Subject: move YearsService.cs to Jellyfin.Api --- MediaBrowser.Api/UserLibrary/YearsService.cs | 54 ---------------------------- 1 file changed, 54 deletions(-) (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs index d023ee90a..f34884f8b 100644 --- a/MediaBrowser.Api/UserLibrary/YearsService.cs +++ b/MediaBrowser.Api/UserLibrary/YearsService.cs @@ -20,27 +20,6 @@ namespace MediaBrowser.Api.UserLibrary { } - /// - /// Class GetYear - /// - [Route("/Years/{Year}", "GET", Summary = "Gets a year")] - public class GetYear : IReturn - { - /// - /// Gets or sets the year. - /// - /// The year. - [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Year { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; } - } - /// /// Class YearsService /// @@ -68,39 +47,6 @@ namespace MediaBrowser.Api.UserLibrary { } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetYear request) - { - var result = GetItem(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets the item. - /// - /// The request. - /// Task{BaseItemDto}. - 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); - } - /// /// Gets the specified request. /// -- cgit v1.2.3 From e72a22c5649d2bb3581b3ee5efd3dbbea370d89c Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 24 Jun 2020 13:48:22 -0600 Subject: Move YearsService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/YearsController.cs | 361 +++++++++++++++++++++------ Jellyfin.Api/Helpers/RequestHelpers.cs | 41 +++ MediaBrowser.Api/UserLibrary/YearsService.cs | 77 ------ 3 files changed, 329 insertions(+), 150 deletions(-) delete mode 100644 MediaBrowser.Api/UserLibrary/YearsService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 2d8ef69cb..a480b78ea 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -1,6 +1,12 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -9,12 +15,31 @@ using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { + /// + /// Years controller. + /// public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public YearsController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } + /// /// Get years. /// @@ -28,80 +53,157 @@ namespace Jellyfin.Api.Controllers /// Optional. Filter by minimum index number. /// Optional. Filter by parent index number. /// Optional. filter by items that have or do not have a parental rating. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// Optional. Filter by items that are HD or not. + /// Optional. Filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. + /// Optional. If specified, results will be excluded based on LocationType. This allows multiple, comma delimited. + /// Optional. Filter by items that are missing episodes or not. + /// Optional. Filter by items that are unaired episodes or not. + /// Optional. Filter by minimum community rating. + /// Optional. Filter by minimum critic rating. + /// Gets all episodes that aired during a season, including specials. + /// Optional. The minimum premiere date. + /// Optional. The minimum last saved date. + /// Optional. The minimum last saved date for user. + /// Optional. The maximum premiere date. + /// Optional. Filter by items that have an overview or not. + /// Optional. Filter by items that have an imdb id or not. + /// Optional. Filter by items that have a tmdb id or not. + /// Optional. Filter by items that have a tvdb id or not. + /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. + /// Skips over a given number of items within the results. Use for paging. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Sort Order - Ascending,Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be included based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Filter by items that are marked as favorite, or not. + /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional. Filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimited. + /// Optional. If specified, results will be excluded based on artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on contributing artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional. Filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. + /// User Id. + /// Optional. Filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional. Filter by items that are locked. + /// Optional. Filter by items that are placeholders. + /// Optional. Filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Min width. + /// Min height. + /// Max width. + /// Max height. + /// Optional. Filter by items that are 3D, or not. + /// Optional. Filter by Series Status. Allows multiple, comma delimited. + /// Optional. Filter by items whose name is sorted equally or greater than a given input string. + /// Optional. Filter by items whose name is sorted equally than a given input string. + /// Optional. Filter by items whose name is equally or lesser than a given input string. + /// Search recursively. + /// Optional. Include image information in output. + /// Return total record count. + /// Year query returned. + /// A containing the year result. [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxOfficialRating", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeSong", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeVideo", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSubtitles", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSpecialFeatures", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTrailer", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "adjacentTo", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minIndexNumber", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "parentIndexNumber", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasParentalRating", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isHd", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is4k", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "locationTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeLocationTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isMissing", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isUnaired", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCommunityRating", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCriticRating", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "airedDuringSeason", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minPremiereDate", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSaved", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSavedForUser", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxPremiereDate", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOverview", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasImdbId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTmdbId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTvdbId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeItemIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "searchTerm", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "filters", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isFavorite", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlayed", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genres", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genreIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "officialRatings", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "tags", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "years", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "person", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albums", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumIds", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "videoTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isLocked", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlaceholder", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOfficialRating", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "collapseBoxSetItems", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minWidth", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minHeight", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxWidth", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxHeight", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is3d", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesStatus", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWithOrGreater", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWith", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameLessThan", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")] public ActionResult> GetYears( [FromQuery] string maxOfficialRating, [FromQuery] bool? hasThemeSong, @@ -186,7 +288,88 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem; + + if (!userId.Equals(Guid.Empty)) + { + user = _userManager.GetUserById(userId); + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); + } + else + { + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); + } + + IList items; + + var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); + var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); + var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypesArr, + IncludeItemTypes = includeItemTypesArr, + MediaTypes = mediaTypesArr, + DtoOptions = dtoOptions + }; + + bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr); + + if (parentItem.IsFolder) + { + var folder = (Folder)parentItem; + + if (!userId.Equals(Guid.Empty)) + { + items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); + } + else + { + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); + } + } + else + { + items = new[] { parentItem }.Where(Filter).ToList(); + } + + var extractedItems = GetAllItems(items); + var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder)); + + var ibnItemsArray = filteredItems.ToList(); + + IEnumerable ibnItems = ibnItemsArray; + + var result = new QueryResult { TotalRecordCount = ibnItemsArray.Count }; + + if (startIndex.HasValue || limit.HasValue) + { + if (startIndex.HasValue) + { + ibnItems = ibnItems.Skip(startIndex.Value); + } + + if (limit.HasValue) + { + ibnItems = ibnItems.Take(limit.Value); + } + } + + var tuples = ibnItems.Select(i => new Tuple>(i, new List())); + + var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); + + result.Items = dtos.Where(i => i != null).ToArray(); + + return result; } /// @@ -222,5 +405,37 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } + + private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes) + { + // Exclude item types + if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Include item types + if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Include MediaTypes + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + private IEnumerable GetAllItems(IEnumerable items) + { + return items + .Select(i => i.ProductionYear ?? 0) + .Where(i => i > 0) + .Distinct() + .Select(year => _libraryManager.GetYear(year)); + } } } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index e2a0cf4fa..6ba62b2e4 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -3,6 +3,7 @@ using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers @@ -91,5 +92,45 @@ namespace Jellyfin.Api.Helpers .Select(i => new Guid(i)) .ToArray(); } + + /// + /// Get orderby. + /// + /// Sort by. + /// Sort order. + /// Resulting order by. + internal static ValueTuple[] GetOrderBy(string sortBy, string requestedSortOrder) + { + var val = sortBy; + + if (string.IsNullOrEmpty(val)) + { + return Array.Empty>(); + } + + var vals = val.Split(','); + if (string.IsNullOrWhiteSpace(requestedSortOrder)) + { + requestedSortOrder = "Ascending"; + } + + var sortOrders = requestedSortOrder.Split(','); + + var result = new ValueTuple[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) + ? SortOrder.Descending + : SortOrder.Ascending; + + result[i] = new ValueTuple(vals[i], sortOrder); + } + + return result; + } } } diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs deleted file mode 100644 index f34884f8b..000000000 --- a/MediaBrowser.Api/UserLibrary/YearsService.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 -{ - /// - /// Class GetYears - /// - [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")] - public class GetYears : GetItemsByName - { - } - - /// - /// Class YearsService - /// - [Authenticated] - public class YearsService : BaseItemsByNameService - { - public YearsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IUserDataManager userDataRepository, - IDtoService dtoService, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - userDataRepository, - dtoService, - authorizationContext) - { - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetYears request) - { - var result = GetResult(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets all items. - /// - /// The request. - /// The items. - /// IEnumerable{Tuple{System.StringFunc{System.Int32}}}. - protected override IEnumerable GetAllItems(GetItemsByName request, IList items) - { - return items - .Select(i => i.ProductionYear ?? 0) - .Where(i => i > 0) - .Distinct() - .Select(year => LibraryManager.GetYear(year)); - } - } -} -- cgit v1.2.3 From 54d666d7c9357901ea76fd405c32c5fcf8ec4431 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 25 Jun 2020 16:46:09 -0600 Subject: move UserViewsService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/UserViewsController.cs | 148 +++++++++++++++++++++ .../Models/UserViewDtos/SpecialViewOptionDto.cs | 18 +++ MediaBrowser.Api/UserLibrary/UserViewsService.cs | 146 -------------------- 3 files changed, 166 insertions(+), 146 deletions(-) create mode 100644 Jellyfin.Api/Controllers/UserViewsController.cs create mode 100644 Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs delete mode 100644 MediaBrowser.Api/UserLibrary/UserViewsService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs new file mode 100644 index 000000000..38bf94087 --- /dev/null +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.UserViewDtos; +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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// User views controller. + /// + public class UserViewsController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly IUserViewManager _userViewManager; + private readonly IDtoService _dtoService; + private readonly IAuthorizationContext _authContext; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserViewsController( + IUserManager userManager, + IUserViewManager userViewManager, + IDtoService dtoService, + IAuthorizationContext authContext, + ILibraryManager libraryManager) + { + _userManager = userManager; + _userViewManager = userViewManager; + _dtoService = dtoService; + _authContext = authContext; + _libraryManager = libraryManager; + } + + /// + /// Get user views. + /// + /// User id. + /// Whether or not to include external views such as channels or live tv. + /// Whether or not to include hidden content. + /// Preset views. + /// User views returned. + /// An containing the user views. + [HttpGet("/Users/{userId}/Views")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetUserViews( + [FromRoute] Guid userId, + [FromQuery] bool? includeExternalContent, + [FromQuery] bool includeHidden, + [FromQuery] string presetViews) + { + var query = new UserViewQuery + { + UserId = userId, + IncludeHidden = includeHidden + }; + + if (includeExternalContent.HasValue) + { + query.IncludeExternalContent = includeExternalContent.Value; + } + + if (!string.IsNullOrWhiteSpace(presetViews)) + { + query.PresetViews = RequestHelpers.Split(presetViews, ',', true); + } + + 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 = new DtoOptions().AddClientFields(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(userId); + + var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) + .ToArray(); + + return new QueryResult + { + Items = dtos, + TotalRecordCount = dtos.Length + }; + } + + /// + /// Get user view grouping options. + /// + /// User id. + /// User view grouping options returned. + /// User not found. + /// + /// An containing the user view grouping options + /// or a if user not found. + /// + [HttpGet("/Users/{userId}/GroupingOptions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetGroupingOptions([FromRoute] Guid userId) + { + var user = _userManager.GetUserById(userId); + if (user == null) + { + return NotFound(); + } + + return Ok(_libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType() + .Where(UserView.IsEligibleForGrouping) + .Select(i => new SpecialViewOptionDto + { + Name = i.Name, + Id = i.Id.ToString("N", CultureInfo.InvariantCulture) + }) + .OrderBy(i => i.Name)); + } + } +} diff --git a/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs new file mode 100644 index 000000000..84b6b0958 --- /dev/null +++ b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.UserViewDtos +{ + /// + /// Special view option dto. + /// + public class SpecialViewOptionDto + { + /// + /// Gets or sets view option name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets view option id. + /// + public string? Id { get; set; } + } +} diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs deleted file mode 100644 index 0fffb0622..000000000 --- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs +++ /dev/null @@ -1,146 +0,0 @@ -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> - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [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 - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [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 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 - { - 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() - .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; } - } -} -- cgit v1.2.3 From 8074c47d294fffabe0d0a423ae98ab6d4917d2b9 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 25 Jun 2020 17:28:12 -0600 Subject: Move UserLibraryService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/UserLibraryController.cs | 391 ++++++++++++++ MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 575 --------------------- 2 files changed, 391 insertions(+), 575 deletions(-) create mode 100644 Jellyfin.Api/Controllers/UserLibraryController.cs delete mode 100644 MediaBrowser.Api/UserLibrary/UserLibraryService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs new file mode 100644 index 000000000..597e70469 --- /dev/null +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// User library controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + public class UserLibraryController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserViewManager _userViewManager; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserLibraryController( + IUserManager userManager, + IUserDataManager userDataRepository, + ILibraryManager libraryManager, + IDtoService dtoService, + IUserViewManager userViewManager, + IFileSystem fileSystem) + { + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _dtoService = dtoService; + _userViewManager = userViewManager; + _fileSystem = fileSystem; + } + + /// + /// Gets an item from a user's library. + /// + /// User id. + /// Item id. + /// Item returned. + /// An containing the d item. + [HttpGet("/Users/{userId}/Items/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); + + var dtoOptions = new DtoOptions().AddClientFields(Request); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Gets the root folder from a user's library. + /// + /// User id. + /// Root folder returned. + /// An containing the user's root folder. + [HttpGet("/Users/{userId}/Items/Root")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetRootFolder([FromRoute] Guid userId) + { + var user = _userManager.GetUserById(userId); + var item = _libraryManager.GetUserRootFolder(); + var dtoOptions = new DtoOptions().AddClientFields(Request); + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Gets intros to play before the main media item plays. + /// + /// User id. + /// Item id. + /// Intros returned. + /// An containing the intros to play. + [HttpGet("/Users/{userId}/Items/{itemId}/Intros")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); + var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); + + return new QueryResult + { + Items = dtos, + TotalRecordCount = dtos.Length + }; + } + + /// + /// Marks an item as a favorite. + /// + /// User id. + /// Item id. + /// Item marked as favorite. + /// An containing the . + [HttpPost("/Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + return MarkFavorite(userId, itemId, true); + } + + /// + /// Unmarks item as a favorite. + /// + /// User id. + /// Item id. + /// Item unmarked as favorite. + /// An containing the . + [HttpDelete("/Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + return MarkFavorite(userId, itemId, false); + } + + /// + /// Deletes a user's saved personal rating for an item. + /// + /// User id. + /// Item id. + /// Personal rating removed. + /// An containing the . + [HttpDelete("/Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + return UpdateUserItemRatingInternal(userId, itemId, null); + } + + /// + /// Updates a user's rating for an item. + /// + /// User id. + /// Item id. + /// Whether this is likes. + /// Item rating updated. + /// An containing the . + [HttpPost("/Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool likes) + { + return UpdateUserItemRatingInternal(userId, itemId, likes); + } + + /// + /// Gets local trailers for an item. + /// + /// User id. + /// Item id. + /// An containing the item's local trailers. + /// The items local trailers. + [HttpGet("/Users/{userId}/Items/{itemId}/LocalTrailers")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions().AddClientFields(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 allTrailers; + } + + return dtosExtras; + } + + /// + /// Gets special features for an item. + /// + /// User id. + /// Item id. + /// Special features returned. + /// An containing the special features. + [HttpGet("/Users/{userId}/Items/{itemId}/SpecialFeatures")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions().AddClientFields(Request); + + return Ok(item + .GetExtras(BaseItem.DisplayExtraTypes) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); + } + + /// + /// Gets latest media. + /// + /// User id. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// 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. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted. + /// Filter by items that are played, or not. + /// Optional. include image information in output. + /// Optional. the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. include user data. + /// Return item limit. + /// Whether or not to group items into a parent container. + /// Latest media returned. + /// An containing the latest media. + [HttpGet("/Users/{userId}/Items/Latest")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLatestMedia( + [FromRoute] Guid userId, + [FromQuery] Guid parentId, + [FromQuery] string fields, + [FromQuery] string includeItemTypes, + [FromQuery] bool? isPlayed, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] bool? enableUserData, + [FromQuery] int limit = 20, + [FromQuery] bool groupItems = true) + { + var user = _userManager.GetUserById(userId); + + if (!isPlayed.HasValue) + { + if (user.HidePlayedInLatest) + { + isPlayed = false; + } + } + + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var list = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + GroupItems = groupItems, + IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + IsPlayed = isPlayed, + Limit = limit, + ParentId = parentId, + UserId = 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 Ok(dtos); + } + + 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); + } + } + } + + /// + /// Marks the favorite. + /// + /// The user id. + /// The item id. + /// if set to true [is favorite]. + 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); + } + + /// + /// Updates the user item rating. + /// + /// The user id. + /// The item id. + /// if set to true [likes]. + private UserItemDataDto UpdateUserItemRatingInternal(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/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs deleted file mode 100644 index f75852885..000000000 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ /dev/null @@ -1,575 +0,0 @@ -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 -{ - /// - /// Class GetItem - /// - [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")] - public class GetItem : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class GetItem - /// - [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")] - public class GetRootFolder : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - } - - /// - /// Class GetIntros - /// - [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")] - public class GetIntros : IReturn> - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the item id. - /// - /// The item id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class MarkFavoriteItem - /// - [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")] - public class MarkFavoriteItem : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - } - - /// - /// Class UnmarkFavoriteItem - /// - [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")] - public class UnmarkFavoriteItem : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - } - - /// - /// Class ClearUserItemRating - /// - [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")] - public class DeleteUserItemRating : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - } - - /// - /// Class UpdateUserItemRating - /// - [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")] - public class UpdateUserItemRating : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - - /// - /// Gets or sets a value indicating whether this is likes. - /// - /// true if likes; otherwise, false. - [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; } - } - - /// - /// Class GetLocalTrailers - /// - [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")] - public class GetLocalTrailers : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - /// - /// Class GetSpecialFeatures - /// - [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")] - public class GetSpecialFeatures : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [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, IHasDtoOptions - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; - } - } - - /// - /// Class UserLibraryService - /// - [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 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; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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(); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task 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); - } - } - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task 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 - { - Items = dtos, - TotalRecordCount = dtos.Length - }; - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified request. - /// - /// The request. - public object Post(MarkFavoriteItem request) - { - var dto = MarkFavorite(request.UserId, request.Id, true); - - return ToOptimizedResult(dto); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public object Delete(UnmarkFavoriteItem request) - { - var dto = MarkFavorite(request.UserId, request.Id, false); - - return ToOptimizedResult(dto); - } - - /// - /// Marks the favorite. - /// - /// The user id. - /// The item id. - /// if set to true [is favorite]. - 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); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public object Delete(DeleteUserItemRating request) - { - var dto = UpdateUserItemRating(request.UserId, request.Id, null); - - return ToOptimizedResult(dto); - } - - /// - /// Posts the specified request. - /// - /// The request. - public object Post(UpdateUserItemRating request) - { - var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes); - - return ToOptimizedResult(dto); - } - - /// - /// Updates the user item rating. - /// - /// The user id. - /// The item id. - /// if set to true [likes]. - 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); - } - } -} -- cgit v1.2.3 From 325808d271a1e53bcf3b15f17b0111c0ce306698 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 25 Jun 2020 18:22:55 -0600 Subject: Move StudiosService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/StudiosController.cs | 277 +++++++++++++++++++++++++ Jellyfin.Api/Helpers/RequestHelpers.cs | 14 ++ MediaBrowser.Api/UserLibrary/StudiosService.cs | 132 ------------ 3 files changed, 291 insertions(+), 132 deletions(-) create mode 100644 Jellyfin.Api/Controllers/StudiosController.cs delete mode 100644 MediaBrowser.Api/UserLibrary/StudiosService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs new file mode 100644 index 000000000..76cf2febf --- /dev/null +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -0,0 +1,277 @@ +using System; +using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Studios controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + public class StudiosController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public StudiosController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } + + /// + /// Gets all studios from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional, include image information in output. + /// Total record count. + /// Studios returned. + /// An containing the studios. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetStudios( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string searchTerm, + [FromQuery] string parentId, + [FromQuery] string fields, + [FromQuery] string excludeItemTypes, + [FromQuery] string includeItemTypes, + [FromQuery] string filters, + [FromQuery] bool? isFavorite, + [FromQuery] string mediaTypes, + [FromQuery] string genres, + [FromQuery] string genreIds, + [FromQuery] string officialRatings, + [FromQuery] string tags, + [FromQuery] string years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] string person, + [FromQuery] string personIds, + [FromQuery] string personTypes, + [FromQuery] string studios, + [FromQuery] string studioIds, + [FromQuery] Guid userId, + [FromQuery] string nameStartsWithOrGreater, + [FromQuery] string nameStartsWith, + [FromQuery] string nameLessThan, + [FromQuery] bool? enableImages = true, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem; + + if (!userId.Equals(Guid.Empty)) + { + user = _userManager.GetUserById(userId); + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); + } + else + { + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); + } + + var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); + var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); + var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypesArr, + IncludeItemTypes = includeItemTypesArr, + MediaTypes = mediaTypesArr, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = RequestHelpers.Split(tags, ',', true), + OfficialRatings = RequestHelpers.Split(officialRatings, ',', true), + Genres = RequestHelpers.Split(genres, ',', true), + GenreIds = RequestHelpers.GetGuids(genreIds), + StudioIds = RequestHelpers.GetGuids(studioIds), + Person = person, + PersonIds = RequestHelpers.GetGuids(personIds), + PersonTypes = RequestHelpers.Split(personTypes, ',', true), + Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount + }; + + if (!string.IsNullOrWhiteSpace(parentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { new Guid(parentId) }; + } + else + { + query.ItemIds = new[] { new Guid(parentId) }; + } + } + + // Studios + if (!string.IsNullOrEmpty(studios)) + { + query.StudioIds = 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 RequestHelpers.GetFilters(filters)) + { + 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 = new QueryResult<(BaseItem, ItemCounts)>(); + var dtos = result.Items.Select(i => + { + var (baseItem, itemCounts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(includeItemTypes)) + { + dto.ChildCount = itemCounts.ItemCount; + dto.ProgramCount = itemCounts.ProgramCount; + dto.SeriesCount = itemCounts.SeriesCount; + dto.EpisodeCount = itemCounts.EpisodeCount; + dto.MovieCount = itemCounts.MovieCount; + dto.TrailerCount = itemCounts.TrailerCount; + dto.AlbumCount = itemCounts.AlbumCount; + dto.SongCount = itemCounts.SongCount; + dto.ArtistCount = itemCounts.ArtistCount; + } + + return dto; + }); + + return new QueryResult + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + /// + /// Gets a studio by name. + /// + /// Studio name. + /// Optional. Filter by user id, and attach user data. + /// Studio returned. + /// An containing the studio. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStudio([FromRoute] string name, [FromQuery] Guid userId) + { + var dtoOptions = new DtoOptions().AddClientFields(Request); + + var item = _libraryManager.GetStudio(name); + if (!userId.Equals(Guid.Empty)) + { + var user = _userManager.GetUserById(userId); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); + } + } +} diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 446ee716a..eec923292 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -3,6 +3,7 @@ using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers @@ -91,5 +92,18 @@ namespace Jellyfin.Api.Helpers .Select(i => new Guid(i)) .ToArray(); } + + /// + /// Gets the filters. + /// + /// The filter string. + /// IEnumerable{ItemFilter}. + internal static ItemFilter[] GetFilters(string filters) + { + return string.IsNullOrEmpty(filters) + ? Array.Empty() + : Split(filters, ',', true) + .Select(v => Enum.Parse(v, true)).ToArray(); + } } } diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs deleted file mode 100644 index 683ce5d09..000000000 --- a/MediaBrowser.Api/UserLibrary/StudiosService.cs +++ /dev/null @@ -1,132 +0,0 @@ -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 -{ - /// - /// Class GetStudios - /// - [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")] - public class GetStudios : GetItemsByName - { - } - - /// - /// Class GetStudio - /// - [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")] - public class GetStudio : IReturn - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; } - } - - /// - /// Class StudiosService - /// - [Authenticated] - public class StudiosService : BaseItemsByNameService - { - public StudiosService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IUserDataManager userDataRepository, - IDtoService dtoService, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - userDataRepository, - dtoService, - authorizationContext) - { - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetStudio request) - { - var result = GetItem(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets the item. - /// - /// The request. - /// Task{BaseItemDto}. - 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); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - - /// - /// Gets all items. - /// - /// The request. - /// The items. - /// IEnumerable{Tuple{System.StringFunc{System.Int32}}}. - protected override IEnumerable GetAllItems(GetItemsByName request, IList items) - { - throw new NotImplementedException(); - } - } -} -- cgit v1.2.3 From f45d44f32150b53231af9651021d1dee690775f1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 26 Jun 2020 21:04:02 -0600 Subject: Move PlaystateService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/PlaystateController.cs | 372 +++++++++++++++++ Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 354 ++++++++++++++++ .../Models/PlaybackDtos/TranscodingJobDto.cs | 256 ++++++++++++ .../Models/PlaybackDtos/TranscodingThrottler.cs | 212 ++++++++++ MediaBrowser.Api/UserLibrary/PlaystateService.cs | 456 --------------------- 5 files changed, 1194 insertions(+), 456 deletions(-) create mode 100644 Jellyfin.Api/Controllers/PlaystateController.cs create mode 100644 Jellyfin.Api/Helpers/TranscodingJobHelper.cs create mode 100644 Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs create mode 100644 Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs delete mode 100644 MediaBrowser.Api/UserLibrary/PlaystateService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs new file mode 100644 index 000000000..05a6edf4e --- /dev/null +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -0,0 +1,372 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Playstate controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + public class PlaystateController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; + private readonly IAuthorizationContext _authContext; + private readonly ILogger _logger; + private readonly TranscodingJobHelper _transcodingJobHelper; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public PlaystateController( + IUserManager userManager, + IUserDataManager userDataRepository, + ILibraryManager libraryManager, + ISessionManager sessionManager, + IAuthorizationContext authContext, + ILoggerFactory loggerFactory, + IMediaSourceManager mediaSourceManager, + IFileSystem fileSystem) + { + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _sessionManager = sessionManager; + _authContext = authContext; + _logger = loggerFactory.CreateLogger(); + + _transcodingJobHelper = new TranscodingJobHelper( + loggerFactory.CreateLogger(), + mediaSourceManager, + fileSystem); + } + + /// + /// Marks an item as played for user. + /// + /// User id. + /// Item id. + /// Optional. The date the item was played. + /// Item marked as played. + /// An containing the . + [HttpPost("/Users/{userId}/PlayedItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult MarkPlayedItem( + [FromRoute] Guid userId, + [FromRoute] Guid itemId, + [FromQuery] DateTime? datePlayed) + { + var user = _userManager.GetUserById(userId); + var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var dto = UpdatePlayedStatus(user, itemId, true, datePlayed); + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + UpdatePlayedStatus(additionalUser, itemId, true, datePlayed); + } + + return dto; + } + + /// + /// Marks an item as unplayed for user. + /// + /// User id. + /// Item id. + /// Item marked as unplayed. + /// A containing the . + [HttpDelete("/Users/{userId}/PlayedItem/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId) + { + var user = _userManager.GetUserById(userId); + var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var dto = UpdatePlayedStatus(user, itemId, false, null); + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + UpdatePlayedStatus(additionalUser, itemId, false, null); + } + + return dto; + } + + /// + /// Reports playback has started within a session. + /// + /// The playback start info. + /// Playback start recorded. + /// A . + [HttpPost("/Sessions/Playing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) + { + playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); + playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports playback progress within a session. + /// + /// The playback progress info. + /// Playback progress recorded. + /// A . + [HttpPost("/Sessions/Playing/Progress")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) + { + playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); + playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Pings a playback session. + /// + /// Playback session id. + /// Playback session pinged. + /// A . + [HttpPost("/Sessions/Playing/Ping")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PingPlaybackSession([FromQuery] string playSessionId) + { + _transcodingJobHelper.PingTranscodingJob(playSessionId, null); + return NoContent(); + } + + /// + /// Reports playback has stopped within a session. + /// + /// The playback stop info. + /// Playback stop recorded. + /// A . + [HttpPost("/Sessions/Playing/Stopped")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo) + { + _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); + if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) + { + await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + } + + playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports that a user has begun playing an item. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// Indicates if the client can seek. + /// The audio stream index. + /// The subtitle stream index. + /// The play method. + /// The live stream id. + /// The play session id. + /// Play start recorded. + /// A . + [HttpPost("/Users/{userId}/PlayingItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackStart( + [FromRoute] Guid userId, + [FromRoute] Guid itemId, + [FromQuery] string mediaSourceId, + [FromQuery] bool canSeek, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] PlayMethod playMethod, + [FromQuery] string liveStreamId, + [FromQuery] string playSessionId) + { + var playbackStartInfo = new PlaybackStartInfo + { + CanSeek = canSeek, + ItemId = itemId, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + PlayMethod = playMethod, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId + }; + + playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); + playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports a user's playback progress. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// Optional. The current position, in ticks. 1 tick = 10000 ms. + /// Indicates if the player is paused. + /// Indicates if the player is muted. + /// The audio stream index. + /// The subtitle stream index. + /// Scale of 0-100. + /// The play method. + /// The live stream id. + /// The play session id. + /// The repeat mode. + /// Play progress recorded. + /// A . + [HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackProgress( + [FromRoute] Guid userId, + [FromRoute] Guid itemId, + [FromQuery] string mediaSourceId, + [FromQuery] long? positionTicks, + [FromQuery] bool isPaused, + [FromQuery] bool isMuted, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? volumeLevel, + [FromQuery] PlayMethod playMethod, + [FromQuery] string liveStreamId, + [FromQuery] string playSessionId, + [FromQuery] RepeatMode repeatMode) + { + var playbackProgressInfo = new PlaybackProgressInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + IsMuted = isMuted, + IsPaused = isPaused, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + VolumeLevel = volumeLevel, + PlayMethod = playMethod, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, + RepeatMode = repeatMode + }; + + playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); + playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports that a user has stopped playing an item. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// The next media type that will play. + /// Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms. + /// The live stream id. + /// The play session id. + /// Playback stop recorded. + /// A . + [HttpDelete("/Users/{userId}/PlayingItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackStopped( + [FromRoute] Guid userId, + [FromRoute] Guid itemId, + [FromQuery] string mediaSourceId, + [FromQuery] string nextMediaType, + [FromQuery] long? positionTicks, + [FromQuery] string liveStreamId, + [FromQuery] string playSessionId) + { + var playbackStopInfo = new PlaybackStopInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + MediaSourceId = mediaSourceId, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, + NextMediaType = nextMediaType + }; + + _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); + if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) + { + await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + } + + playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Updates the played status. + /// + /// The user. + /// The item id. + /// if set to true [was played]. + /// The date played. + /// Task. + private UserItemDataDto UpdatePlayedStatus(User user, Guid 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); + } + + private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId) + { + if (method == PlayMethod.Transcode) + { + var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId); + if (job == null) + { + return PlayMethod.DirectPlay; + } + } + + return method; + } + } +} diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs new file mode 100644 index 000000000..44f662e6e --- /dev/null +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.PlaybackDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Transcoding job helpers. + /// + public class TranscodingJobHelper + { + /// + /// The active transcoding jobs. + /// + private static readonly List _activeTranscodingJobs = new List(); + + /// + /// The transcoding locks. + /// + private static readonly Dictionary _transcodingLocks = new Dictionary(); + + private readonly ILogger _logger; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TranscodingJobHelper( + ILogger logger, + IMediaSourceManager mediaSourceManager, + IFileSystem fileSystem) + { + _logger = logger; + _mediaSourceManager = mediaSourceManager; + _fileSystem = fileSystem; + } + + /// + /// Get transcoding job. + /// + /// Playback session id. + /// The transcoding job. + public TranscodingJobDto GetTranscodingJob(string playSessionId) + { + lock (_activeTranscodingJobs) + { + return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase)); + } + } + + /// + /// Ping transcoding job. + /// + /// Play session id. + /// Is user paused. + /// Play session id is null. + public void PingTranscodingJob(string playSessionId, bool? isUserPaused) + { + if (string.IsNullOrEmpty(playSessionId)) + { + throw new ArgumentNullException(nameof(playSessionId)); + } + + _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); + + List jobs; + + lock (_activeTranscodingJobs) + { + // This is really only needed for HLS. + // Progressive streams can stop on their own reliably + jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + foreach (var job in jobs) + { + if (isUserPaused.HasValue) + { + _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id); + job.IsUserPaused = isUserPaused.Value; + } + + PingTimer(job, true); + } + } + + private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn) + { + if (job.HasExited) + { + job.StopKillTimer(); + return; + } + + var timerDuration = 10000; + + if (job.Type != TranscodingJobType.Progressive) + { + timerDuration = 60000; + } + + job.PingTimeout = timerDuration; + job.LastPingDate = DateTime.UtcNow; + + // Don't start the timer for playback checkins with progressive streaming + if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn) + { + job.StartKillTimer(OnTranscodeKillTimerStopped); + } + else + { + job.ChangeKillTimerIfStarted(); + } + } + + /// + /// Called when [transcode kill timer stopped]. + /// + /// The state. + private async void OnTranscodeKillTimerStopped(object state) + { + var job = (TranscodingJobDto)state; + + if (!job.HasExited && job.Type != TranscodingJobType.Progressive) + { + var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds; + + if (timeSinceLastPing < job.PingTimeout) + { + job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout); + return; + } + } + + _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + + await KillTranscodingJob(job, true, path => true).ConfigureAwait(false); + } + + /// + /// Kills the single transcoding job. + /// + /// The device id. + /// The play session identifier. + /// The delete files. + /// Task. + public Task KillTranscodingJobs(string deviceId, string playSessionId, Func deleteFiles) + { + return KillTranscodingJobs( + j => string.IsNullOrWhiteSpace(playSessionId) + ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) + : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles); + } + + /// + /// Kills the transcoding jobs. + /// + /// The kill job. + /// The delete files. + /// Task. + private Task KillTranscodingJobs(Func killJob, Func deleteFiles) + { + var jobs = new List(); + + lock (_activeTranscodingJobs) + { + // This is really only needed for HLS. + // Progressive streams can stop on their own reliably + jobs.AddRange(_activeTranscodingJobs.Where(killJob)); + } + + if (jobs.Count == 0) + { + return Task.CompletedTask; + } + + IEnumerable GetKillJobs() + { + foreach (var job in jobs) + { + yield return KillTranscodingJob(job, false, deleteFiles); + } + } + + return Task.WhenAll(GetKillJobs()); + } + + /// + /// Kills the transcoding job. + /// + /// The job. + /// if set to true [close live stream]. + /// The delete. + private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func delete) + { + job.DisposeKillTimer(); + + _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + + lock (_activeTranscodingJobs) + { + _activeTranscodingJobs.Remove(job); + + if (!job.CancellationTokenSource!.IsCancellationRequested) + { + job.CancellationTokenSource.Cancel(); + } + } + + lock (_transcodingLocks) + { + _transcodingLocks.Remove(job.Path!); + } + + lock (job.ProcessLock!) + { + job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); + + var process = job.Process; + + var hasExited = job.HasExited; + + if (!hasExited) + { + try + { + _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path); + + process!.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous + if (!process.WaitForExit(5000)) + { + _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path); + process.Kill(); + } + } + catch (InvalidOperationException) + { + } + } + } + + if (delete(job.Path!)) + { + await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); + } + + if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId)) + { + try + { + await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error closing live stream for {Path}", job.Path); + } + } + } + + private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs) + { + if (retryCount >= 10) + { + return; + } + + _logger.LogInformation("Deleting partial stream file(s) {Path}", path); + + await Task.Delay(delayMs).ConfigureAwait(false); + + try + { + if (jobType == TranscodingJobType.Progressive) + { + DeleteProgressivePartialStreamFiles(path); + } + else + { + DeleteHlsPartialStreamFiles(path); + } + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); + + await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); + } + } + + /// + /// Deletes the progressive partial stream files. + /// + /// The output file path. + private void DeleteProgressivePartialStreamFiles(string outputFilePath) + { + if (File.Exists(outputFilePath)) + { + _fileSystem.DeleteFile(outputFilePath); + } + } + + /// + /// Deletes the HLS partial stream files. + /// + /// The output file path. + private void DeleteHlsPartialStreamFiles(string outputFilePath) + { + var directory = Path.GetDirectoryName(outputFilePath); + var name = Path.GetFileNameWithoutExtension(outputFilePath); + + var filesToDelete = _fileSystem.GetFilePaths(directory) + .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1); + + List? exs = null; + foreach (var file in filesToDelete) + { + try + { + _logger.LogDebug("Deleting HLS file {0}", file); + _fileSystem.DeleteFile(file); + } + catch (IOException ex) + { + (exs ??= new List(4)).Add(ex); + _logger.LogError(ex, "Error deleting HLS file {Path}", file); + } + } + + if (exs != null) + { + throw new AggregateException("Error deleting HLS files", exs); + } + } + } +} diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs new file mode 100644 index 000000000..dcc322470 --- /dev/null +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dto; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Models.PlaybackDtos +{ + /// + /// Class TranscodingJob. + /// + public class TranscodingJobDto + { + /// + /// The process lock. + /// + [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] + public readonly object ProcessLock = new object(); + + /// + /// Timer lock. + /// + private readonly object _timerLock = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public TranscodingJobDto(ILogger logger) + { + Logger = logger; + } + + /// + /// Gets or sets the play session identifier. + /// + /// The play session identifier. + public string? PlaySessionId { get; set; } + + /// + /// Gets or sets the live stream identifier. + /// + /// The live stream identifier. + public string? LiveStreamId { get; set; } + + /// + /// Gets or sets a value indicating whether is live output. + /// + public bool IsLiveOutput { get; set; } + + /// + /// Gets or sets the path. + /// + /// The path. + public MediaSourceInfo? MediaSource { get; set; } + + /// + /// Gets or sets path. + /// + public string? Path { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public TranscodingJobType Type { get; set; } + + /// + /// Gets or sets the process. + /// + /// The process. + public Process? Process { get; set; } + + /// + /// Gets logger. + /// + public ILogger Logger { get; private set; } + + /// + /// Gets or sets the active request count. + /// + /// The active request count. + public int ActiveRequestCount { get; set; } + + /// + /// Gets or sets the kill timer. + /// + /// The kill timer. + private Timer? KillTimer { get; set; } + + /// + /// Gets or sets device id. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets cancellation token source. + /// + public CancellationTokenSource? CancellationTokenSource { get; set; } + + /// + /// Gets or sets a value indicating whether has exited. + /// + public bool HasExited { get; set; } + + /// + /// Gets or sets a value indicating whether is user paused. + /// + public bool IsUserPaused { get; set; } + + /// + /// Gets or sets id. + /// + public string? Id { get; set; } + + /// + /// Gets or sets framerate. + /// + public float? Framerate { get; set; } + + /// + /// Gets or sets completion percentage. + /// + public double? CompletionPercentage { get; set; } + + /// + /// Gets or sets bytes downloaded. + /// + public long? BytesDownloaded { get; set; } + + /// + /// Gets or sets bytes transcoded. + /// + public long? BytesTranscoded { get; set; } + + /// + /// Gets or sets bit rate. + /// + public int? BitRate { get; set; } + + /// + /// Gets or sets transcoding position ticks. + /// + public long? TranscodingPositionTicks { get; set; } + + /// + /// Gets or sets download position ticks. + /// + public long? DownloadPositionTicks { get; set; } + + /// + /// Gets or sets transcoding throttler. + /// + public TranscodingThrottler? TranscodingThrottler { get; set; } + + /// + /// Gets or sets last ping date. + /// + public DateTime LastPingDate { get; set; } + + /// + /// Gets or sets ping timeout. + /// + public int PingTimeout { get; set; } + + /// + /// Stop kill timer. + /// + public void StopKillTimer() + { + lock (_timerLock) + { + KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + /// + /// Dispose kill timer. + /// + public void DisposeKillTimer() + { + lock (_timerLock) + { + if (KillTimer != null) + { + KillTimer.Dispose(); + KillTimer = null; + } + } + } + + /// + /// Start kill timer. + /// + /// Callback action. + public void StartKillTimer(Action callback) + { + StartKillTimer(callback, PingTimeout); + } + + /// + /// Start kill timer. + /// + /// Callback action. + /// Callback interval. + public void StartKillTimer(Action callback, int intervalMs) + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + if (KillTimer == null) + { + Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); + } + else + { + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + /// + /// Change kill timer if started. + /// + public void ChangeKillTimerIfStarted() + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + if (KillTimer != null) + { + var intervalMs = PingTimeout; + + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + } +} diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs new file mode 100644 index 000000000..b5e42ea29 --- /dev/null +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -0,0 +1,212 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Models.PlaybackDtos +{ + /// + /// Transcoding throttler. + /// + public class TranscodingThrottler : IDisposable + { + private readonly TranscodingJobDto _job; + private readonly ILogger _logger; + private readonly IConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private Timer? _timer; + private bool _isPaused; + + /// + /// Initializes a new instance of the class. + /// + /// Transcoding job dto. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TranscodingThrottler(TranscodingJobDto job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem) + { + _job = job; + _logger = logger; + _config = config; + _fileSystem = fileSystem; + } + + /// + /// Start timer. + /// + public void Start() + { + _timer = new Timer(TimerCallback, null, 5000, 5000); + } + + /// + /// Unpause transcoding. + /// + /// A . + public async Task UnpauseTranscoding() + { + if (_isPaused) + { + _logger.LogDebug("Sending resume command to ffmpeg"); + + try + { + await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false); + _isPaused = false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resuming transcoding"); + } + } + } + + /// + /// Stop throttler. + /// + /// A . + public async Task Stop() + { + DisposeTimer(); + await UnpauseTranscoding().ConfigureAwait(false); + } + + /// + /// Dispose throttler. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose throttler. + /// + /// Disposing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposeTimer(); + } + } + + private EncodingOptions GetOptions() + { + return _config.GetConfiguration("encoding"); + } + + private async void TimerCallback(object state) + { + if (_job.HasExited) + { + DisposeTimer(); + return; + } + + var options = GetOptions(); + + if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds)) + { + await PauseTranscoding().ConfigureAwait(false); + } + else + { + await UnpauseTranscoding().ConfigureAwait(false); + } + } + + private async Task PauseTranscoding() + { + if (!_isPaused) + { + _logger.LogDebug("Sending pause command to ffmpeg"); + + try + { + await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false); + _isPaused = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error pausing transcoding"); + } + } + } + + private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) + { + var bytesDownloaded = job.BytesDownloaded ?? 0; + var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; + var downloadPositionTicks = job.DownloadPositionTicks ?? 0; + + var path = job.Path; + var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks; + + if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) + { + // HLS - time-based consideration + + var targetGap = gapLengthInTicks; + var gap = transcodingPositionTicks - downloadPositionTicks; + + if (gap < targetGap) + { + _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); + return false; + } + + _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); + return true; + } + + if (bytesDownloaded > 0 && transcodingPositionTicks > 0) + { + // Progressive Streaming - byte-based consideration + + try + { + var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length; + + // Estimate the bytes the transcoder should be ahead + double gapFactor = gapLengthInTicks; + gapFactor /= transcodingPositionTicks; + var targetGap = bytesTranscoded * gapFactor; + + var gap = bytesTranscoded - bytesDownloaded; + + if (gap < targetGap) + { + _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return false; + } + + _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting output size"); + return false; + } + } + + _logger.LogDebug("No throttle data for " + path); + return false; + } + + private void DisposeTimer() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs deleted file mode 100644 index ab231626b..000000000 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ /dev/null @@ -1,456 +0,0 @@ -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 -{ - /// - /// Class MarkPlayedItem - /// - [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")] - public class MarkPlayedItem : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; } - - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - /// - /// Class MarkUnplayedItem - /// - [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")] - public class MarkUnplayedItem : IReturn - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [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 - { - } - - /// - /// Class OnPlaybackStart - /// - [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")] - public class OnPlaybackStart : IReturnVoid - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [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; } - } - - /// - /// Class OnPlaybackProgress - /// - [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")] - public class OnPlaybackProgress : IReturnVoid - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [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; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - [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; } - } - - /// - /// Class OnPlaybackStopped - /// - [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")] - public class OnPlaybackStopped : IReturnVoid - { - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string UserId { get; set; } - - /// - /// Gets or sets the id. - /// - /// The id. - [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; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - [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 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; - } - - /// - /// Posts the specified request. - /// - /// The request. - 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; - } - - /// - /// Posts the specified request. - /// - /// The request. - 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); - } - - /// - /// Posts the specified request. - /// - /// The request. - 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); - } - - /// - /// Posts the specified request. - /// - /// The request. - 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); - } - - /// - /// Deletes the specified request. - /// - /// The request. - 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; - } - - /// - /// Updates the played status. - /// - /// The user. - /// The item id. - /// if set to true [was played]. - /// The date played. - /// Task. - 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); - } - } -} -- cgit v1.2.3 From 45e034e9a1e02fc1ac2632222a7f232a2008ce08 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 29 Jun 2020 16:34:00 +0200 Subject: Move ArtistsService to Jellyfin.Api --- Jellyfin.Api/Controllers/ArtistsController.cs | 488 +++++++++++++++++++++++++ Jellyfin.Api/Helpers/RequestHelpers.cs | 14 + MediaBrowser.Api/UserLibrary/ArtistsService.cs | 143 -------- 3 files changed, 502 insertions(+), 143 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ArtistsController.cs delete mode 100644 MediaBrowser.Api/UserLibrary/ArtistsService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs new file mode 100644 index 000000000..ae81818b8 --- /dev/null +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The artists controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + [Route("/Artists")] + public class ArtistsController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ArtistsController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } + + /// + /// Gets all artists from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional, include image information in output. + /// Total record count. + /// Artists returned. + /// An containing the artists. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetArtists( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string searchTerm, + [FromQuery] string parentId, + [FromQuery] string fields, + [FromQuery] string excludeItemTypes, + [FromQuery] string includeItemTypes, + [FromQuery] string filters, + [FromQuery] bool? isFavorite, + [FromQuery] string mediaTypes, + [FromQuery] string genres, + [FromQuery] string genreIds, + [FromQuery] string officialRatings, + [FromQuery] string tags, + [FromQuery] string years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] string person, + [FromQuery] string personIds, + [FromQuery] string personTypes, + [FromQuery] string studios, + [FromQuery] string studioIds, + [FromQuery] Guid userId, + [FromQuery] string nameStartsWithOrGreater, + [FromQuery] string nameStartsWith, + [FromQuery] string nameLessThan, + [FromQuery] bool? enableImages = true, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem; + + if (!userId.Equals(Guid.Empty)) + { + user = _userManager.GetUserById(userId); + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); + } + else + { + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); + } + + var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); + var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); + var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypesArr, + IncludeItemTypes = includeItemTypesArr, + MediaTypes = mediaTypesArr, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = RequestHelpers.Split(tags, ',', true), + OfficialRatings = RequestHelpers.Split(officialRatings, ',', true), + Genres = RequestHelpers.Split(genres, ',', true), + GenreIds = RequestHelpers.GetGuids(genreIds), + StudioIds = RequestHelpers.GetGuids(studioIds), + Person = person, + PersonIds = RequestHelpers.GetGuids(personIds), + PersonTypes = RequestHelpers.Split(personTypes, ',', true), + Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount + }; + + if (!string.IsNullOrWhiteSpace(parentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { new Guid(parentId) }; + } + else + { + query.ItemIds = new[] { new Guid(parentId) }; + } + } + + // Studios + if (!string.IsNullOrEmpty(studios)) + { + query.StudioIds = 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 RequestHelpers.GetFilters(filters)) + { + 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 = _libraryManager.GetArtists(query); + + var dtos = result.Items.Select(i => + { + var (baseItem, itemCounts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(includeItemTypes)) + { + dto.ChildCount = itemCounts.ItemCount; + dto.ProgramCount = itemCounts.ProgramCount; + dto.SeriesCount = itemCounts.SeriesCount; + dto.EpisodeCount = itemCounts.EpisodeCount; + dto.MovieCount = itemCounts.MovieCount; + dto.TrailerCount = itemCounts.TrailerCount; + dto.AlbumCount = itemCounts.AlbumCount; + dto.SongCount = itemCounts.SongCount; + dto.ArtistCount = itemCounts.ArtistCount; + } + + return dto; + }); + + return new QueryResult + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + /// + /// Gets all album artists from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional, include image information in output. + /// Total record count. + /// Album artists returned. + /// An containing the album artists. + [HttpGet("/AlbumArtists")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAlbumArtists( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string searchTerm, + [FromQuery] string parentId, + [FromQuery] string fields, + [FromQuery] string excludeItemTypes, + [FromQuery] string includeItemTypes, + [FromQuery] string filters, + [FromQuery] bool? isFavorite, + [FromQuery] string mediaTypes, + [FromQuery] string genres, + [FromQuery] string genreIds, + [FromQuery] string officialRatings, + [FromQuery] string tags, + [FromQuery] string years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] string person, + [FromQuery] string personIds, + [FromQuery] string personTypes, + [FromQuery] string studios, + [FromQuery] string studioIds, + [FromQuery] Guid userId, + [FromQuery] string nameStartsWithOrGreater, + [FromQuery] string nameStartsWith, + [FromQuery] string nameLessThan, + [FromQuery] bool? enableImages = true, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem; + + if (!userId.Equals(Guid.Empty)) + { + user = _userManager.GetUserById(userId); + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); + } + else + { + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); + } + + var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); + var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); + var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypesArr, + IncludeItemTypes = includeItemTypesArr, + MediaTypes = mediaTypesArr, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = RequestHelpers.Split(tags, ',', true), + OfficialRatings = RequestHelpers.Split(officialRatings, ',', true), + Genres = RequestHelpers.Split(genres, ',', true), + GenreIds = RequestHelpers.GetGuids(genreIds), + StudioIds = RequestHelpers.GetGuids(studioIds), + Person = person, + PersonIds = RequestHelpers.GetGuids(personIds), + PersonTypes = RequestHelpers.Split(personTypes, ',', true), + Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount + }; + + if (!string.IsNullOrWhiteSpace(parentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { new Guid(parentId) }; + } + else + { + query.ItemIds = new[] { new Guid(parentId) }; + } + } + + // Studios + if (!string.IsNullOrEmpty(studios)) + { + query.StudioIds = 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 RequestHelpers.GetFilters(filters)) + { + 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 = _libraryManager.GetAlbumArtists(query); + + var dtos = result.Items.Select(i => + { + var (baseItem, itemCounts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(includeItemTypes)) + { + dto.ChildCount = itemCounts.ItemCount; + dto.ProgramCount = itemCounts.ProgramCount; + dto.SeriesCount = itemCounts.SeriesCount; + dto.EpisodeCount = itemCounts.EpisodeCount; + dto.MovieCount = itemCounts.MovieCount; + dto.TrailerCount = itemCounts.TrailerCount; + dto.AlbumCount = itemCounts.AlbumCount; + dto.SongCount = itemCounts.SongCount; + dto.ArtistCount = itemCounts.ArtistCount; + } + + return dto; + }); + + return new QueryResult + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + /// + /// Gets an artist by name. + /// + /// Studio name. + /// Optional. Filter by user id, and attach user data. + /// Artist returned. + /// An containing the artist. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetArtistByName([FromRoute] string name, [FromQuery] Guid userId) + { + var dtoOptions = new DtoOptions().AddClientFields(Request); + + var item = _libraryManager.GetArtist(name, dtoOptions); + + if (!userId.Equals(Guid.Empty)) + { + var user = _userManager.GetUserById(userId); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); + } + } +} diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index fd86feb8b..dec8fd95e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -4,6 +4,7 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Helpers @@ -130,5 +131,18 @@ namespace Jellyfin.Api.Helpers return result; } + + /// + /// Gets the filters. + /// + /// The filter string. + /// IEnumerable{ItemFilter}. + internal static ItemFilter[] GetFilters(string filters) + { + return string.IsNullOrEmpty(filters) + ? Array.Empty() + : Split(filters, ',', true) + .Select(v => Enum.Parse(v, true)).ToArray(); + } } } diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs deleted file mode 100644 index bef91d54d..000000000 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ /dev/null @@ -1,143 +0,0 @@ -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 -{ - /// - /// Class GetArtists - /// - [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 - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; } - } - - /// - /// Class ArtistsService - /// - [Authenticated] - public class ArtistsService : BaseItemsByNameService - { - public ArtistsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IUserDataManager userDataRepository, - IDtoService dtoService, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - userDataRepository, - dtoService, - authorizationContext) - { - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetArtist request) - { - return GetItem(request); - } - - /// - /// Gets the item. - /// - /// The request. - /// Task{BaseItemDto}. - 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); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetArtists request) - { - return GetResultSlim(request); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - - /// - /// Gets all items. - /// - /// The request. - /// The items. - /// IEnumerable{Tuple{System.StringFunc{System.Int32}}}. - protected override IEnumerable GetAllItems(GetItemsByName request, IList items) - { - throw new NotImplementedException(); - } - } -} -- cgit v1.2.3 From 1aaee5cf8f78fe00defd78ad7521496772c7b27b Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 30 Jun 2020 18:26:53 -0600 Subject: Move PersonsService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/PersonsController.cs | 282 +++++++++++++++++++++++++ MediaBrowser.Api/UserLibrary/PersonsService.cs | 146 ------------- 2 files changed, 282 insertions(+), 146 deletions(-) create mode 100644 Jellyfin.Api/Controllers/PersonsController.cs delete mode 100644 MediaBrowser.Api/UserLibrary/PersonsService.cs (limited to 'MediaBrowser.Api/UserLibrary') diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs new file mode 100644 index 000000000..03478a20a --- /dev/null +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -0,0 +1,282 @@ +using System; +using System.Globalization; +using System.Linq; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Persons controller. + /// + public class PersonsController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public PersonsController( + ILibraryManager libraryManager, + IDtoService dtoService, + IUserManager userManager) + { + _libraryManager = libraryManager; + _dtoService = dtoService; + _userManager = userManager; + } + + /// + /// Gets all persons from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// The search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional, include image information in output. + /// Optional. Include total record count. + /// Persons returned. + /// An containing the queryresult of persons. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPersons( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string searchTerm, + [FromQuery] string parentId, + [FromQuery] string fields, + [FromQuery] string excludeItemTypes, + [FromQuery] string includeItemTypes, + [FromQuery] string filters, + [FromQuery] bool? isFavorite, + [FromQuery] string mediaTypes, + [FromQuery] string genres, + [FromQuery] string genreIds, + [FromQuery] string officialRatings, + [FromQuery] string tags, + [FromQuery] string years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery] string enableImageTypes, + [FromQuery] string person, + [FromQuery] string personIds, + [FromQuery] string personTypes, + [FromQuery] string studios, + [FromQuery] string studioIds, + [FromQuery] Guid userId, + [FromQuery] string nameStartsWithOrGreater, + [FromQuery] string nameStartsWith, + [FromQuery] string nameLessThan, + [FromQuery] bool? enableImages = true, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem; + + if (!userId.Equals(Guid.Empty)) + { + user = _userManager.GetUserById(userId); + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId); + } + else + { + parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); + } + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = RequestHelpers.Split(tags, '|', true), + OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), + Genres = RequestHelpers.Split(genres, '|', true), + GenreIds = RequestHelpers.GetGuids(genreIds), + StudioIds = RequestHelpers.GetGuids(studioIds), + Person = person, + PersonIds = RequestHelpers.GetGuids(personIds), + PersonTypes = RequestHelpers.Split(personTypes, ',', true), + Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(), + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount + }; + + if (!string.IsNullOrWhiteSpace(parentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { new Guid(parentId) }; + } + else + { + query.ItemIds = new[] { new Guid(parentId) }; + } + } + + // Studios + if (!string.IsNullOrEmpty(studios)) + { + query.StudioIds = 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 RequestHelpers.GetFilters(filters)) + { + 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 = new QueryResult<(BaseItem, ItemCounts)>(); + + var dtos = result.Items.Select(i => + { + var (baseItem, counts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(includeItemTypes)) + { + 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; + } + + return dto; + }); + + return new QueryResult + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + /// + /// Get person by name. + /// + /// Person name. + /// Optional. Filter by user id, and attach user data. + /// Person returned. + /// Person not found. + /// An containing the person on success, + /// or a if person not found. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetPerson([FromRoute] string name, [FromQuery] Guid userId) + { + var dtoOptions = new DtoOptions() + .AddClientFields(Request); + + var item = _libraryManager.GetPerson(name); + if (item == null) + { + return NotFound(); + } + + if (!userId.Equals(Guid.Empty)) + { + var user = _userManager.GetUserById(userId); + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs deleted file mode 100644 index 3204e5219..000000000 --- a/MediaBrowser.Api/UserLibrary/PersonsService.cs +++ /dev/null @@ -1,146 +0,0 @@ -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 -{ - /// - /// Class GetPersons - /// - [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")] - public class GetPersons : GetItemsByName - { - } - - /// - /// Class GetPerson - /// - [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")] - public class GetPerson : IReturn - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [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; } - } - - /// - /// Class PersonsService - /// - [Authenticated] - public class PersonsService : BaseItemsByNameService - { - public PersonsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IUserDataManager userDataRepository, - IDtoService dtoService, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - userDataRepository, - dtoService, - authorizationContext) - { - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPerson request) - { - var result = GetItem(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets the item. - /// - /// The request. - /// Task{BaseItemDto}. - 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); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPersons request) - { - return GetResultSlim(request); - } - - /// - /// Gets all items. - /// - /// The request. - /// The items. - /// IEnumerable{Tuple{System.StringFunc{System.Int32}}}. - protected override IEnumerable GetAllItems(GetItemsByName request, IList 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() - }; - } - } -} -- cgit v1.2.3