using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Mime; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// Live tv controller. /// public class LiveTvController : BaseJellyfinApiController { private readonly ILiveTvManager _liveTvManager; private readonly IUserManager _userManager; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly ISessionContext _sessionContext; private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; 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. /// Instance of the class. public LiveTvController( ILiveTvManager liveTvManager, IUserManager userManager, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IDtoService dtoService, ISessionContext sessionContext, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, TranscodingJobHelper transcodingJobHelper) { _liveTvManager = liveTvManager; _userManager = userManager; _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _dtoService = dtoService; _sessionContext = sessionContext; _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; _transcodingJobHelper = transcodingJobHelper; } /// /// Gets available live tv services. /// /// Available live tv services returned. /// /// An containing the available live tv services. /// [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult GetLiveTvInfo() { return _liveTvManager.GetLiveTvInfo(CancellationToken.None); } /// /// Gets available live tv channels. /// /// Optional. Filter by channel type. /// Optional. Filter by user and attach user data. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. Filter for movies. /// Optional. Filter for series. /// Optional. Filter for news. /// Optional. Filter for kids. /// Optional. Filter for sports. /// Optional. The maximum number of records to return. /// Optional. Filter by channels that are favorites, or not. /// Optional. Filter by channels that are liked, or not. /// Optional. Filter by channels that are disliked, 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. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Key to sort by. /// Optional. Sort order. /// Optional. Incorporate favorite and like status into channel sorting. /// Optional. Adds current program info to each channel. /// Available live tv channels returned. /// /// An containing the resulting available live tv channels. /// [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, [FromQuery] int? limit, [FromQuery] bool? isFavorite, [FromQuery] bool? isLiked, [FromQuery] bool? isDisliked, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) { var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var channelResult = _liveTvManager.GetInternalChannels( new LiveTvChannelQuery { ChannelType = type, UserId = userId ?? Guid.Empty, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, IsLiked = isLiked, IsDisliked = isDisliked, EnableFavoriteSorting = enableFavoriteSorting, IsMovie = isMovie, IsSeries = isSeries, IsNews = isNews, IsKids = isKids, IsSports = isSports, SortBy = RequestHelpers.Split(sortBy, ',', true), SortOrder = sortOrder ?? SortOrder.Ascending, AddCurrentProgram = addCurrentProgram }, dtoOptions, CancellationToken.None); var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var fieldsList = dtoOptions.Fields.ToList(); fieldsList.Remove(ItemFields.CanDelete); fieldsList.Remove(ItemFields.CanDownload); fieldsList.Remove(ItemFields.DisplayPreferencesId); fieldsList.Remove(ItemFields.Etag); dtoOptions.Fields = fieldsList.ToArray(); dtoOptions.AddCurrentProgram = addCurrentProgram; var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user); return new QueryResult { Items = returnArray, TotalRecordCount = channelResult.TotalRecordCount }; } /// /// Gets a live tv channel. /// /// Channel id. /// Optional. Attach user data. /// Live tv channel returned. /// An containing the live tv channel. [HttpGet("Channels/{channelId}")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var item = channelId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(channelId); var dtoOptions = new DtoOptions() .AddClientFields(Request); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } /// /// Gets live tv recordings. /// /// Optional. Filter by channel id. /// Optional. Filter by user and attach user data. /// 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. Filter by recording status. /// Optional. Filter by recordings that are in progress, or not. /// Optional. Filter by recordings belonging to a series timer. /// 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. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Filter for movies. /// Optional. Filter for series. /// Optional. Filter for kids. /// Optional. Filter for sports. /// Optional. Filter for news. /// Optional. Filter for is library item. /// Optional. Return total record count. /// Live tv recordings returned. /// An containing the live tv recordings. [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] RecordingStatus? status, [FromQuery] bool? isInProgress, [FromQuery] string? seriesTimerId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, [FromQuery] bool? isKids, [FromQuery] bool? isSports, [FromQuery] bool? isNews, [FromQuery] bool? isLibraryItem, [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return _liveTvManager.GetRecordings( new RecordingQuery { ChannelId = channelId, UserId = userId ?? Guid.Empty, StartIndex = startIndex, Limit = limit, Status = status, SeriesTimerId = seriesTimerId, IsInProgress = isInProgress, EnableTotalRecordCount = enableTotalRecordCount, IsMovie = isMovie, IsNews = isNews, IsSeries = isSeries, IsKids = isKids, IsSports = isSports, IsLibraryItem = isLibraryItem, Fields = fields, ImageTypeLimit = imageTypeLimit, EnableImages = enableImages }, dtoOptions); } /// /// Gets live tv recording series. /// /// Optional. Filter by channel id. /// Optional. Filter by user and attach user data. /// Optional. Filter by recording group. /// 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. Filter by recording status. /// Optional. Filter by recordings that are in progress, or not. /// Optional. Filter by recordings belonging to a series timer. /// 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. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Return total record count. /// Live tv recordings returned. /// An containing the live tv recordings. [HttpGet("Recordings/Series")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")] public ActionResult> GetRecordingsSeries( [FromQuery] string? channelId, [FromQuery] Guid? userId, [FromQuery] string? groupId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] RecordingStatus? status, [FromQuery] bool? isInProgress, [FromQuery] string? seriesTimerId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { return new QueryResult(); } /// /// Gets live tv recording groups. /// /// Optional. Filter by user and attach user data. /// Recording groups returned. /// An containing the recording groups. [HttpGet("Recordings/Groups")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) { return new QueryResult(); } /// /// Gets recording folders. /// /// Optional. Filter by user and attach user data. /// Recording folders returned. /// An containing the recording folders. [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var folders = _liveTvManager.GetRecordingFolders(user); var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); return new QueryResult { Items = returnArray, TotalRecordCount = returnArray.Count }; } /// /// Gets a live tv recording. /// /// Recording id. /// Optional. Attach user data. /// Recording returned. /// An containing the live tv recording. [HttpGet("Recordings/{recordingId}")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var dtoOptions = new DtoOptions() .AddClientFields(Request); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } /// /// Resets a tv tuner. /// /// Tuner id. /// Tuner reset. /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult ResetTuner([FromRoute, Required] string tunerId) { AssertUserCanManageLiveTv(); _liveTvManager.ResetTuner(tunerId, CancellationToken.None); return NoContent(); } /// /// Gets a timer. /// /// Timer id. /// Timer returned. /// /// A containing an which contains the timer. /// [HttpGet("Timers/{timerId}")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task> GetTimer([FromRoute, Required] string timerId) { return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); } /// /// Gets the default values for a new timer. /// /// Optional. To attach default values based on a program. /// Default values returned. /// /// A containing an which contains the default values for a timer. /// [HttpGet("Timers/Defaults")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task> GetDefaultTimer([FromQuery] string? programId) { return string.IsNullOrEmpty(programId) ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false) : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false); } /// /// Gets the live tv timers. /// /// Optional. Filter by channel id. /// Optional. Filter by timers belonging to a series timer. /// Optional. Filter by timers that are active. /// Optional. Filter by timers that are scheduled. /// /// A containing an which contains the live tv timers. /// [HttpGet("Timers")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task>> GetTimers( [FromQuery] string? channelId, [FromQuery] string? seriesTimerId, [FromQuery] bool? isActive, [FromQuery] bool? isScheduled) { return await _liveTvManager.GetTimers( new TimerQuery { ChannelId = channelId, SeriesTimerId = seriesTimerId, IsActive = isActive, IsScheduled = isScheduled }, CancellationToken.None) .ConfigureAwait(false); } /// /// Gets available live tv epgs. /// /// The channels to return guide information for. /// Optional. Filter by user id. /// Optional. The minimum premiere start date. /// Optional. Filter by programs that have completed airing, or not. /// Optional. Filter by programs that are currently airing, or not. /// Optional. The maximum premiere start date. /// Optional. The minimum premiere end date. /// Optional. The maximum premiere end date. /// Optional. Filter for movies. /// Optional. Filter for series. /// Optional. Filter for news. /// Optional. Filter for kids. /// Optional. Filter for sports. /// 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 one or more sort orders, comma delimited. Options: Name, StartDate. /// Sort Order - Ascending,Descending. /// The genres to return guide information for. /// The genre ids to return guide information for. /// 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. Filter by series timer id. /// Optional. Filter by library series id. /// Optional. Specify additional fields of information to return in the output. /// Retrieve total record count. /// Live tv epgs returned. /// /// A containing a which contains the live tv epgs. /// [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task>> GetLiveTvPrograms( [FromQuery] string? channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, [FromQuery] bool? hasAired, [FromQuery] bool? isAiring, [FromQuery] DateTime? maxStartDate, [FromQuery] DateTime? minEndDate, [FromQuery] DateTime? maxEndDate, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? sortBy, [FromQuery] string? sortOrder, [FromQuery] string? genres, [FromQuery] string? genreIds, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] string? seriesTimerId, [FromQuery] Guid? librarySeriesId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var query = new InternalItemsQuery(user) { ChannelIds = RequestHelpers.Split(channelIds, ',', true) .Select(i => new Guid(i)).ToArray(), HasAired = hasAired, IsAiring = isAiring, EnableTotalRecordCount = enableTotalRecordCount, MinStartDate = minStartDate, MinEndDate = minEndDate, MaxStartDate = maxStartDate, MaxEndDate = maxEndDate, StartIndex = startIndex, Limit = limit, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsNews = isNews, IsMovie = isMovie, IsSeries = isSeries, IsKids = isKids, IsSports = isSports, SeriesTimerId = seriesTimerId, Genres = RequestHelpers.Split(genres, '|', true), GenreIds = RequestHelpers.GetGuids(genreIds) }; if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) { query.IsSeries = true; if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series) { query.Name = series.Name; } } var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } /// /// Gets available live tv epgs. /// /// Request body. /// Live tv epgs returned. /// /// A containing a which contains the live tv epgs. /// [HttpPost("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task>> GetPrograms([FromBody] GetProgramsDto body) { var user = body.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(body.UserId); var query = new InternalItemsQuery(user) { ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) .Select(i => new Guid(i)).ToArray(), HasAired = body.HasAired, IsAiring = body.IsAiring, EnableTotalRecordCount = body.EnableTotalRecordCount, MinStartDate = body.MinStartDate, MinEndDate = body.MinEndDate, MaxStartDate = body.MaxStartDate, MaxEndDate = body.MaxEndDate, StartIndex = body.StartIndex, Limit = body.Limit, OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder), IsNews = body.IsNews, IsMovie = body.IsMovie, IsSeries = body.IsSeries, IsKids = body.IsKids, IsSports = body.IsSports, SeriesTimerId = body.SeriesTimerId, Genres = RequestHelpers.Split(body.Genres, '|', true), GenreIds = RequestHelpers.GetGuids(body.GenreIds) }; if (!body.LibrarySeriesId.Equals(Guid.Empty)) { query.IsSeries = true; if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series) { query.Name = series.Name; } } var dtoOptions = new DtoOptions { Fields = body.Fields } .AddClientFields(Request) .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } /// /// Gets recommended live tv epgs. /// /// Optional. filter by user id. /// Optional. The maximum number of records to return. /// Optional. Filter by programs that are currently airing, or not. /// Optional. Filter by programs that have completed airing, or not. /// Optional. Filter for series. /// Optional. Filter for movies. /// Optional. Filter for news. /// Optional. Filter for kids. /// Optional. Filter for sports. /// 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. /// The genres to return guide information for. /// Optional. Specify additional fields of information to return in the output. /// Optional. include user data. /// Retrieve total record count. /// Recommended epgs returned. /// A containing the queryresult of recommended epgs. [HttpGet("Programs/Recommended")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRecommendedPrograms( [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery] bool? isAiring, [FromQuery] bool? hasAired, [FromQuery] bool? isSeries, [FromQuery] bool? isMovie, [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; var query = new InternalItemsQuery(user) { IsAiring = isAiring, Limit = limit, HasAired = hasAired, IsSeries = isSeries, IsMovie = isMovie, IsKids = isKids, IsNews = isNews, IsSports = isSports, EnableTotalRecordCount = enableTotalRecordCount, GenreIds = RequestHelpers.GetGuids(genreIds) }; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None); } /// /// Gets a live tv program. /// /// Program id. /// Optional. Attach user data. /// Program returned. /// An containing the livetv program. [HttpGet("Programs/{programId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetProgram( [FromRoute, Required] string programId, [FromQuery] Guid? userId) { var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); } /// /// Deletes a live tv recording. /// /// Recording id. /// Recording deleted. /// Item not found. /// A on success, or a if item not found. [HttpDelete("Recordings/{recordingId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) { AssertUserCanManageLiveTv(); var item = _libraryManager.GetItemById(recordingId); if (item == null) { return NotFound(); } _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }); return NoContent(); } /// /// Cancels a live tv timer. /// /// Timer id. /// Timer deleted. /// A . [HttpDelete("Timers/{timerId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelTimer([FromRoute, Required] string timerId) { AssertUserCanManageLiveTv(); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); return NoContent(); } /// /// Updates a live tv timer. /// /// Timer id. /// New timer info. /// Timer updated. /// A . [HttpPost("Timers/{timerId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) { AssertUserCanManageLiveTv(); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Creates a live tv timer. /// /// New timer info. /// Timer created. /// A . [HttpPost("Timers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) { AssertUserCanManageLiveTv(); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Gets a live tv series timer. /// /// Timer id. /// Series timer returned. /// Series timer not found. /// A on success, or a if timer not found. [HttpGet("SeriesTimers/{timerId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetSeriesTimer([FromRoute, Required] string timerId) { var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false); if (timer == null) { return NotFound(); } return timer; } /// /// Gets live tv series timers. /// /// Optional. Sort by SortName or Priority. /// Optional. Sort in Ascending or Descending order. /// Timers returned. /// An of live tv series timers. [HttpGet("SeriesTimers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) { return await _liveTvManager.GetSeriesTimers( new SeriesTimerQuery { SortOrder = sortOrder ?? SortOrder.Ascending, SortBy = sortBy }, CancellationToken.None).ConfigureAwait(false); } /// /// Cancels a live tv series timer. /// /// Timer id. /// Timer cancelled. /// A . [HttpDelete("SeriesTimers/{timerId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelSeriesTimer([FromRoute, Required] string timerId) { AssertUserCanManageLiveTv(); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); return NoContent(); } /// /// Updates a live tv series timer. /// /// Timer id. /// New series timer info. /// Series timer updated. /// A . [HttpPost("SeriesTimers/{timerId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) { AssertUserCanManageLiveTv(); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Creates a live tv series timer. /// /// New series timer info. /// Series timer info created. /// A . [HttpPost("SeriesTimers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { AssertUserCanManageLiveTv(); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Get recording group. /// /// Group id. /// A . [HttpGet("Recordings/Groups/{groupId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) { return NotFound(); } /// /// Get guid info. /// /// Guid info returned. /// An containing the guide info. [HttpGet("GuideInfo")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGuideInfo() { return _liveTvManager.GetGuideInfo(); } /// /// Adds a tuner host. /// /// New tuner host. /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) { return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); } /// /// Deletes a tuner host. /// /// Tuner host id. /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { var config = _configurationManager.GetConfiguration("livetv"); config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); _configurationManager.SaveConfiguration("livetv", config); return NoContent(); } /// /// Gets default listings provider info. /// /// Default listings provider info returned. /// An containing the default listings provider info. [HttpGet("ListingProviders/Default")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultListingProvider() { return new ListingsProviderInfo(); } /// /// Adds a listings provider. /// /// Password. /// New listings info. /// Validate listings. /// Validate login. /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( [FromQuery] string? pw, [FromBody] ListingsProviderInfo listingsProviderInfo, [FromQuery] bool validateListings = false, [FromQuery] bool validateLogin = false) { if (!string.IsNullOrEmpty(pw)) { using var sha = SHA1.Create(); listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))); } return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); } /// /// Delete listing provider. /// /// Listing provider id. /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { _liveTvManager.DeleteListingsProvider(id); return NoContent(); } /// /// Gets available lineups. /// /// Provider id. /// Provider type. /// Location. /// Country. /// Available lineups returned. /// A containing the available lineups. [HttpGet("ListingProviders/Lineups")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetLineups( [FromQuery] string? id, [FromQuery] string? type, [FromQuery] string? location, [FromQuery] string? country) { return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false); } /// /// Gets available countries. /// /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() { var client = _httpClientFactory.CreateClient(NamedClient.Default); // https://json.schedulesdirect.org/20141201/available/countries // Can't dispose the response as it's required up the call chain. var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries")) .ConfigureAwait(false); return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json); } /// /// Get channel mapping options. /// /// Provider id. /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetChannelMappingOptions([FromQuery] string? providerId) { var config = _configurationManager.GetConfiguration("livetv"); var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name; var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None) .ConfigureAwait(false); var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None) .ConfigureAwait(false); var mappings = listingsProviderInfo.ChannelMappings; return new ChannelMappingOptionsDto { TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(), ProviderChannels = providerChannels.Select(i => new NameIdPair { Name = i.Name, Id = i.Id }).ToList(), Mappings = mappings, ProviderName = listingsProviderName }; } /// /// Set channel mappings. /// /// Provider id. /// Tuner channel id. /// Provider channel id. /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SetChannelMapping( [FromQuery] string? providerId, [FromQuery] string? tunerChannelId, [FromQuery] string? providerChannelId) { return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false); } /// /// Get tuner host types. /// /// Tuner host types returned. /// An containing the tuner host types. [HttpGet("TunerHosts/Types")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetTunerHostTypes() { return _liveTvManager.GetTunerHostTypes(); } /// /// Discover tuners. /// /// Only discover new tuners. /// Tuners returned. /// An containing the tuners. [HttpGet("Tuners/Discvover")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) { return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false); } /// /// Gets a live tv recording stream. /// /// Recording id. /// Recording stream returned. /// Recording not found. /// /// An containing the recording stream on success, /// or a if recording not found. /// [HttpGet("LiveRecordings/{recordingId}/stream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesVideoFile] public async Task GetLiveRecordingFile([FromRoute, Required] string recordingId) { var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); if (string.IsNullOrWhiteSpace(path)) { return NotFound(); } await using var memoryStream = new MemoryStream(); await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None) .WriteToAsync(memoryStream, CancellationToken.None) .ConfigureAwait(false); return File(memoryStream, MimeTypes.GetMimeType(path)); } /// /// Gets a live tv channel stream. /// /// Stream id. /// Container type. /// Stream returned. /// Stream not found. /// /// An containing the channel stream on success, /// or a if stream not found. /// [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesVideoFile] public async Task GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container) { var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false); if (liveStreamInfo == null) { return NotFound(); } var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper); return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } private void AssertUserCanManageLiveTv() { var user = _sessionContext.GetUser(Request); if (user == null) { throw new SecurityException("Anonymous live tv management is not allowed."); } if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) { throw new SecurityException("The current user does not have permission to manage live tv."); } } } }