using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.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. /// 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. /// Optional. Starting date of shows to show in Next Up section. /// Whether to enable the total records count. Defaults to true. /// Whether to disable sending the first episode in a series as next up. /// Whether to get a rewatching next up instead of standard next up. /// A with the next up episodes. [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNextUp( [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? seriesId, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool disableFirstEpisode = false, [FromQuery] bool rewatching = false) { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var result = _tvSeriesManager.GetNextUp( new NextUpQuery { Limit = limit, ParentId = parentId, SeriesId = seriesId, StartIndex = startIndex, UserId = userId ?? Guid.Empty, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, Rewatching = rewatching }, options); var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); return new QueryResult( startIndex, result.TotalRecordCount, 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. /// 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1); var parentIdGuid = parentId ?? Guid.Empty; var options = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, Limit = limit, ParentId = parentIdGuid, Recursive = true, DtoOptions = options }); var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); return new QueryResult( startIndex, itemsResult.Count, 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 delimited. 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 delimited. 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, Required] Guid seriesId, [FromQuery] Guid? userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] int? season, [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, [FromQuery] string? adjacentTo, [FromQuery] Guid? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); List episodes; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { var item = _libraryManager.GetItemById(seasonId.Value); if (item is not 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 not 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 not 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 (startItemId.HasValue) { episodes = episodes .SkipWhile(i => !startItemId.Value.Equals(i.Id)) .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( startIndex, episodes.Count, 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 delimited. 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, Required] Guid seriesId, [FromQuery] Guid? userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, [FromQuery] string? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } var seasons = series.GetItemList(new InternalItemsQuery(user) { IsMissing = isMissing, IsSpecialSeason = isSpecialSeason, AdjacentTo = adjacentTo }); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); return new QueryResult(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; } } }