using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// The tv shows controller. /// [Route("/Shows")] [Authorize(Policy = Policies.DefaultAuthorization)] public class TvShowsController : BaseJellyfinApiController { private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly ITVSeriesManager _tvSeriesManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public TvShowsController( IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService, ITVSeriesManager tvSeriesManager) { _userManager = userManager; _libraryManager = libraryManager; _dtoService = dtoService; _tvSeriesManager = tvSeriesManager; } /// /// Gets a list of next up episodes. /// /// The user id of the user to get the next up episodes for. /// 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. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Optional. Filter by series id. /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. /// 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. /// Whether to enable the total records count. Defaults to true. /// A with the next up episodes. [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNextUp( [FromQuery] Guid userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, [FromQuery] string? seriesId, [FromQuery] string? parentId, [FromQuery] bool? enableImges, [FromQuery] int? imageTypeLimit, [FromQuery] string? enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { var options = new DtoOptions() .AddItemFields(fields!) .AddClientFields(Request) .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); var result = _tvSeriesManager.GetNextUp( new NextUpQuery { Limit = limit, ParentId = parentId, SeriesId = seriesId, StartIndex = startIndex, UserId = userId, EnableTotalRecordCount = enableTotalRecordCount }, options); var user = _userManager.GetUserById(userId); var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); return new QueryResult { TotalRecordCount = result.TotalRecordCount, Items = returnItems }; } /// /// Gets a list of upcoming episodes. /// /// The user id of the user to get the upcoming episodes for. /// 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. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. /// 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. /// A with the next up episodes. [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUpcomingEpisodes( [FromQuery] Guid userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, [FromQuery] string? parentId, [FromQuery] bool? enableImges, [FromQuery] int? imageTypeLimit, [FromQuery] string? enableImageTypes, [FromQuery] bool? enableUserData) { var user = _userManager.GetUserById(userId); var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1); var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); var options = new DtoOptions() .AddItemFields(fields!) .AddClientFields(Request) .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { nameof(Episode) }, OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Ascending)).ToArray(), MinPremiereDate = minPremiereDate, StartIndex = startIndex, Limit = limit, ParentId = parentIdGuid, Recursive = true, DtoOptions = options }); var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); return new QueryResult { TotalRecordCount = itemsResult.Count, Items = returnItems }; } /// /// Gets episodes for a tv season. /// /// The series id. /// The user id. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Optional filter by season number. /// Optional. Filter by season id. /// Optional. Filter by items that are missing episodes or not. /// Optional. Return items that are siblings of a supplied item. /// Optional. Skip through the list until a given item is found. /// 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, 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. /// Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. /// A with the episodes on success or a if the series was not found. [HttpGet("{seriesId}/Episodes")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetEpisodes( [FromRoute] string? seriesId, [FromQuery] Guid userId, [FromQuery] string? fields, [FromQuery] int? season, [FromQuery] string? seasonId, [FromQuery] bool? isMissing, [FromQuery] string? adjacentTo, [FromQuery] string? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery] string? enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { var user = _userManager.GetUserById(userId); List episodes; var dtoOptions = new DtoOptions() .AddItemFields(fields!) .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id. { var item = _libraryManager.GetItemById(new Guid(seasonId)); if (!(item is Season seasonItem)) { return NotFound("No season exists with Id " + seasonId); } episodes = seasonItem.GetEpisodes(user, dtoOptions); } else if (season.HasValue) // Season number was supplied. Get episodes by season number { if (!(_libraryManager.GetItemById(seriesId) is Series series)) { return NotFound("Series not found"); } var seasonItem = series .GetSeasons(user, dtoOptions) .FirstOrDefault(i => i.IndexNumber == season.Value); episodes = seasonItem == null ? new List() : ((Season)seasonItem).GetEpisodes(user, dtoOptions); } else // No season number or season id was supplied. Returning all episodes. { if (!(_libraryManager.GetItemById(seriesId) is Series series)) { return NotFound("Series not found"); } episodes = series.GetEpisodes(user, dtoOptions).ToList(); } // Filter after the fact in case the ui doesn't want them if (isMissing.HasValue) { var val = isMissing.Value; episodes = episodes .Where(i => ((Episode)i).IsMissingEpisode == val) .ToList(); } if (!string.IsNullOrWhiteSpace(startItemId)) { episodes = episodes .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase)) .ToList(); } // This must be the last filter if (!string.IsNullOrEmpty(adjacentTo)) { episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList(); } if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) { episodes.Shuffle(); } var returnItems = episodes; if (startIndex.HasValue || limit.HasValue) { returnItems = ApplyPaging(episodes, startIndex, limit).ToList(); } var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); return new QueryResult { TotalRecordCount = episodes.Count, Items = dtos }; } /// /// Gets seasons for a tv series. /// /// The series id. /// The user id. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Optional. Filter by special season. /// Optional. Filter by items that are missing episodes or not. /// Optional. Return items that are siblings of a supplied item. /// 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. /// A on success or a if the series was not found. [HttpGet("{seriesId}/Seasons")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetSeasons( [FromRoute] string? seriesId, [FromQuery] Guid userId, [FromQuery] string? fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, [FromQuery] string? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery] string? enableImageTypes, [FromQuery] bool? enableUserData) { var user = _userManager.GetUserById(userId); if (!(_libraryManager.GetItemById(seriesId) is Series series)) { return NotFound("Series not found"); } var seasons = series.GetItemList(new InternalItemsQuery(user) { IsMissing = isMissing, IsSpecialSeason = isSpecialSeason, AdjacentTo = adjacentTo }); var dtoOptions = new DtoOptions() .AddItemFields(fields) .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); return new QueryResult { TotalRecordCount = returnItems.Count, Items = returnItems }; } /// /// Applies the paging. /// /// The items. /// The start index. /// The limit. /// IEnumerable{BaseItem}. private IEnumerable ApplyPaging(IEnumerable items, int? startIndex, int? limit) { // Start at if (startIndex.HasValue) { items = items.Skip(startIndex.Value); } // Return limit if (limit.HasValue) { items = items.Take(limit.Value); } return items; } } }