diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
44 files changed, 281 insertions, 597 deletions
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index c3d02976eb..a19a203b51 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 991f8cbf20..3363d7bad2 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c9d2f67f92..e7d3e694ab 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) @@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 968193a6f8..5bc5330861 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -15,7 +15,6 @@ namespace Jellyfin.Api.Controllers; /// <summary> /// The audio controller. /// </summary> -// TODO: In order to authenticate this in the future, Dlna playback will require updating public class AudioController : BaseJellyfinApiController { private readonly AudioHelper _audioHelper; @@ -95,7 +94,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -147,7 +146,6 @@ public class AudioController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -260,7 +258,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -312,7 +310,6 @@ public class AudioController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 11c4ac3768..fdc16ee23f 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2db04afb80..2d9f1ed69a 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Collections; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 9007dfc410..8db22f7ebe 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index aa0dff2123..aa200a7221 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs deleted file mode 100644 index 415385463d..0000000000 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Dlna; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers; - -/// <summary> -/// Dlna Controller. -/// </summary> -[Authorize(Policy = Policies.RequiresElevation)] -public class DlnaController : BaseJellyfinApiController -{ - private readonly IDlnaManager _dlnaManager; - - /// <summary> - /// Initializes a new instance of the <see cref="DlnaController"/> class. - /// </summary> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - public DlnaController(IDlnaManager dlnaManager) - { - _dlnaManager = dlnaManager; - } - - /// <summary> - /// Get profile infos. - /// </summary> - /// <response code="200">Device profile infos returned.</response> - /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns> - [HttpGet("ProfileInfos")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos() - { - return Ok(_dlnaManager.GetProfileInfos()); - } - - /// <summary> - /// Gets the default profile. - /// </summary> - /// <response code="200">Default device profile returned.</response> - /// <returns>An <see cref="OkResult"/> containing the default profile.</returns> - [HttpGet("Profiles/Default")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<DeviceProfile> GetDefaultProfile() - { - return _dlnaManager.GetDefaultProfile(); - } - - /// <summary> - /// Gets a single profile. - /// </summary> - /// <param name="profileId">Profile Id.</param> - /// <response code="200">Device profile returned.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns> - [HttpGet("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId) - { - var profile = _dlnaManager.GetProfile(profileId); - if (profile is null) - { - return NotFound(); - } - - return profile; - } - - /// <summary> - /// Deletes a profile. - /// </summary> - /// <param name="profileId">Profile id.</param> - /// <response code="204">Device profile deleted.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> - [HttpDelete("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteProfile([FromRoute, Required] string profileId) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.DeleteProfile(profileId); - return NoContent(); - } - - /// <summary> - /// Creates a profile. - /// </summary> - /// <param name="deviceProfile">Device profile.</param> - /// <response code="204">Device profile created.</response> - /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpPost("Profiles")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) - { - _dlnaManager.CreateProfile(deviceProfile); - return NoContent(); - } - - /// <summary> - /// Updates a profile. - /// </summary> - /// <param name="profileId">Profile id.</param> - /// <param name="deviceProfile">Device profile.</param> - /// <response code="204">Device profile updated.</response> - /// <response code="404">Device profile not found.</response> - /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> - [HttpPost("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.UpdateProfile(profileId, deviceProfile); - return NoContent(); - } -} diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs deleted file mode 100644 index 42576934b3..0000000000 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Net.Mime; -using System.Threading.Tasks; -using Emby.Dlna; -using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Net; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers; - -/// <summary> -/// Dlna Server Controller. -/// </summary> -[Route("Dlna")] -[DlnaEnabled] -[Authorize(Policy = Policies.AnonymousLanAccessPolicy)] -public class DlnaServerController : BaseJellyfinApiController -{ - private readonly IDlnaManager _dlnaManager; - private readonly IContentDirectory _contentDirectory; - private readonly IConnectionManager _connectionManager; - private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; - - /// <summary> - /// Initializes a new instance of the <see cref="DlnaServerController"/> class. - /// </summary> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param> - /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param> - /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param> - public DlnaServerController( - IDlnaManager dlnaManager, - IContentDirectory contentDirectory, - IConnectionManager connectionManager, - IMediaReceiverRegistrar mediaReceiverRegistrar) - { - _dlnaManager = dlnaManager; - _contentDirectory = contentDirectory; - _connectionManager = connectionManager; - _mediaReceiverRegistrar = mediaReceiverRegistrar; - } - - /// <summary> - /// Get Description Xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Description xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> - [HttpGet("{serverId}/description")] - [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId) - { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); - } - - /// <summary> - /// Gets Dlna content directory xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna content directory returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> - [HttpGet("{serverId}/ContentDirectory")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId) - { - return Ok(_contentDirectory.GetServiceXml()); - } - - /// <summary> - /// Gets Dlna media receiver registrar xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna media receiver registrar xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Dlna media receiver registrar xml.</returns> - [HttpGet("{serverId}/MediaReceiverRegistrar")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId) - { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); - } - - /// <summary> - /// Gets Dlna media receiver registrar xml. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Dlna media receiver registrar xml returned.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Dlna media receiver registrar xml.</returns> - [HttpGet("{serverId}/ConnectionManager")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId) - { - return Ok(_connectionManager.GetServiceXml()); - } - - /// <summary> - /// Process a content directory control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/ContentDirectory/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); - } - - /// <summary> - /// Process a connection manager control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/ConnectionManager/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); - } - - /// <summary> - /// Process a media receiver registrar control request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Control response.</returns> - [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) - { - return ProcessEventRequest(_mediaReceiverRegistrar); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/ContentDirectory/Events")] - [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) - { - return ProcessEventRequest(_contentDirectory); - } - - /// <summary> - /// Processes an event subscription request. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <response code="200">Request processed.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Event subscription response.</returns> - [HttpSubscribe("{serverId}/ConnectionManager/Events")] - [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) - { - return ProcessEventRequest(_connectionManager); - } - - /// <summary> - /// Gets a server icon. - /// </summary> - /// <param name="serverId">Server UUID.</param> - /// <param name="fileName">The icon filename.</param> - /// <response code="200">Request processed.</response> - /// <response code="404">Not Found.</response> - /// <response code="503">DLNA is disabled.</response> - /// <returns>Icon stream.</returns> - [HttpGet("{serverId}/icons/{fileName}")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - /// <summary> - /// Gets a server icon. - /// </summary> - /// <param name="fileName">The icon filename.</param> - /// <returns>Icon stream.</returns> - /// <response code="200">Request processed.</response> - /// <response code="404">Not Found.</response> - /// <response code="503">DLNA is disabled.</response> - [HttpGet("icons/{fileName}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIcon([FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - private ActionResult GetIconInternal(string fileName) - { - var icon = _dlnaManager.GetIcon(fileName); - if (icon is null) - { - return NotFound(); - } - - return File(icon.Stream, MimeTypes.GetMimeType(fileName)); - } - - private string GetAbsoluteUri() - { - return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; - } - - private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) - { - return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) - { - InputXml = requestStream, - TargetServerUuId = id, - RequestedUrl = GetAbsoluteUri() - }); - } - - private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager) - { - var subscriptionId = Request.Headers["SID"]; - if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) - { - var notificationType = Request.Headers["NT"]; - var callback = Request.Headers["CALLBACK"]; - var timeoutString = Request.Headers["TIMEOUT"]; - - if (string.IsNullOrEmpty(notificationType)) - { - return dlnaEventManager.RenewEventSubscription( - subscriptionId, - notificationType, - timeoutString, - callback); - } - - return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback); - } - - return dlnaEventManager.CancelEventSubscription(subscriptionId); - } -} diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 42c94c29d3..9e9c610cc5 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -17,8 +17,6 @@ using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Encoder; @@ -49,12 +47,10 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; private readonly EncodingHelper _encodingHelper; @@ -67,12 +63,10 @@ public class DynamicHlsController : BaseJellyfinApiController /// </summary> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> @@ -81,12 +75,10 @@ public class DynamicHlsController : BaseJellyfinApiController public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, - IDlnaManager dlnaManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, DynamicHlsHelper dynamicHlsHelper, @@ -95,12 +87,10 @@ public class DynamicHlsController : BaseJellyfinApiController { _libraryManager = libraryManager; _userManager = userManager; - _dlnaManager = dlnaManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; @@ -176,7 +166,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -231,7 +221,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -294,8 +283,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationToken) @@ -410,6 +397,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> + /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns> [HttpGet("Videos/{itemId}/master.m3u8")] @@ -421,7 +409,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -467,7 +455,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, - [FromQuery] bool enableAdaptiveBitrateStreaming = true) + [FromQuery] bool enableAdaptiveBitrateStreaming = true, + [FromQuery] bool enableTrickplay = true) { var streamingRequest = new HlsVideoRequestDto { @@ -475,7 +464,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -521,7 +509,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming + EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, + EnableTrickplay = enableTrickplay }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -591,7 +580,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -644,7 +633,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -757,7 +745,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -811,7 +799,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -925,7 +912,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -978,7 +965,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1102,7 +1088,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -1158,7 +1144,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1283,7 +1268,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -1338,7 +1323,6 @@ public class DynamicHlsController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -1399,8 +1383,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationTokenSource.Token) @@ -1439,8 +1421,6 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, TranscodingJobType, cancellationToken) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 8c9ee1a19e..284a97621d 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.EnvironmentDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; @@ -168,7 +169,7 @@ public class EnvironmentController : BaseJellyfinApiController // Check if unc share var index = path.LastIndexOf(UncSeparator); - if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + if (index != -1 && path[0] == UncSeparator) { parent = path.Substring(0, index); diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index d51a5325f5..baeb8b81a0 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index da60f2c60b..6cb1993e46 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) @@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder - && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) + && (parentCollectionFolder.CollectionType == CollectionType.music + || parentCollectionFolder.CollectionType == CollectionType.musicvideos)) { result = _libraryManager.GetMusicGenres(query); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 6eedfd8c7f..392d9955fb 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -160,7 +160,7 @@ public class HlsSegmentController : BaseJellyfinApiController var pathExtension = Path.GetExtension(path); if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) - && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + && path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase)) { playlistPath = path; break; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7b10ea170f..c031ce338d 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; @@ -79,7 +80,7 @@ public class ImageController : BaseJellyfinApiController _appPaths = appPaths; } - private static Stream GetFromBase64Stream(Stream inputStream) + private static CryptoStream GetFromBase64Stream(Stream inputStream) => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read); /// <summary> @@ -2079,30 +2080,30 @@ public class ImageController : BaseJellyfinApiController foreach (var (key, value) in headers) { - Response.Headers.Add(key, value); + Response.Headers.Append(key, value); } Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; - Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); - Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept); if (disableCaching) { - Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); - Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); } else { if (cacheDuration.HasValue) { - Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); + Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); } else { - Response.Headers.Add(HeaderNames.CacheControl, "public"); + Response.Headers.Append(HeaderNames.CacheControl, "public"); } - Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); + Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index b030e74dda..e3aee1bf7a 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index b8f6e91ad2..0a8522e1cf 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 504f2fa1d7..9800248c68 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -164,18 +166,16 @@ public class ItemUpdateController : BaseJellyfinApiController var inheritedContentType = _libraryManager.GetInheritedContentType(item); var configuredContentType = _libraryManager.GetConfiguredContentType(item); - if (string.IsNullOrWhiteSpace(inheritedContentType) || - !string.IsNullOrWhiteSpace(configuredContentType)) + if (inheritedContentType is null || configuredContentType is not null) { info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentType = configuredContentType; - if (string.IsNullOrWhiteSpace(inheritedContentType) - || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows) { info.ContentTypeOptions = info.ContentTypeOptions .Where(i => string.IsNullOrWhiteSpace(i.Value) - || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase)) .ToArray(); } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80128536da..a1fc8e11b2 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -34,6 +34,7 @@ public class ItemsController : BaseJellyfinApiController private readonly IDtoService _dtoService; private readonly ILogger<ItemsController> _logger; private readonly ISessionManager _sessionManager; + private readonly IUserDataManager _userDataRepository; /// <summary> /// Initializes a new instance of the <see cref="ItemsController"/> class. @@ -44,13 +45,15 @@ public class ItemsController : BaseJellyfinApiController /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> + /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> public ItemsController( IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService, ILogger<ItemsController> logger, - ISessionManager sessionManager) + ISessionManager sessionManager, + IUserDataManager userDataRepository) { _userManager = userManager; _libraryManager = libraryManager; @@ -58,6 +61,7 @@ public class ItemsController : BaseJellyfinApiController _dtoService = dtoService; _logger = logger; _sessionManager = sessionManager; + _userDataRepository = userDataRepository; } /// <summary> @@ -195,9 +199,9 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -269,13 +273,13 @@ public class ItemsController : BaseJellyfinApiController folder = _libraryManager.GetUserRootFolder(); } - string? collectionType = null; + CollectionType? collectionType = null; if (folder is IHasCollectionType hasCollectionType) { collectionType = hasCollectionType.CollectionType; } - if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.playlists) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; @@ -652,9 +656,9 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -812,7 +816,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -881,4 +885,64 @@ public class ItemsController : BaseJellyfinApiController itemsResult.TotalRecordCount, returnItems); } + + /// <summary> + /// Get Item User Data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <response code="200">return item user data.</response> + /// <response code="404">Item is not found.</response> + /// <returns>Return <see cref="UserItemDataDto"/>.</returns> + [HttpGet("Users/{userId}/Items/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<UserItemDataDto> GetItemUserData( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data."); + } + + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); + var item = _libraryManager.GetItemById(itemId); + + return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user); + } + + /// <summary> + /// Update Item User Data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="userDataDto">New user data object.</param> + /// <response code="200">return updated user item data.</response> + /// <response code="404">Item is not found.</response> + /// <returns>Return <see cref="UserItemDataDto"/>.</returns> + [HttpPost("Users/{userId}/Items/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<UserItemDataDto> UpdateItemUserData( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromBody, Required] UpdateUserItemDataDto userDataDto) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data."); + } + + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData); + + return _userDataRepository.GetUserDataDto(item, user); + } } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 46c0a8d527..de057bbab9 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; @@ -294,8 +295,8 @@ public class LibraryController : BaseJellyfinApiController return new AllThemeMediaResult { - ThemeSongsResult = themeSongs?.Value, - ThemeVideosResult = themeVideos?.Value, + ThemeSongsResult = themeSongs.Value, + ThemeVideosResult = themeVideos.Value, SoundtrackSongsResult = new ThemeMediaResult() }; } @@ -490,7 +491,7 @@ public class LibraryController : BaseJellyfinApiController baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - parent = parent?.GetParent(); + parent = parent.GetParent(); } return baseItemDtos; @@ -788,7 +789,7 @@ public class LibraryController : BaseJellyfinApiController [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo( - [FromQuery] string? libraryContentType, + [FromQuery] CollectionType? libraryContentType, [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); @@ -922,19 +923,19 @@ public class LibraryController : BaseJellyfinApiController } } - private static string[] GetRepresentativeItemTypes(string? contentType) + private static string[] GetRepresentativeItemTypes(CollectionType? contentType) { return contentType switch { - CollectionType.BoxSets => new[] { "BoxSet" }, - CollectionType.Playlists => new[] { "Playlist" }, - CollectionType.Movies => new[] { "Movie" }, - CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, - CollectionType.Books => new[] { "Book" }, - CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, - CollectionType.HomeVideos => new[] { "Video", "Photo" }, - CollectionType.Photos => new[] { "Video", "Photo" }, - CollectionType.MusicVideos => new[] { "MusicVideo" }, + CollectionType.boxsets => new[] { "BoxSet" }, + CollectionType.playlists => new[] { "Playlist" }, + CollectionType.movies => new[] { "Movie" }, + CollectionType.tvshows => new[] { "Series", "Season", "Episode" }, + CollectionType.books => new[] { "Book" }, + CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, + CollectionType.homevideos => new[] { "Video", "Photo" }, + CollectionType.photos => new[] { "Video", "Photo" }, + CollectionType.musicvideos => new[] { "MusicVideo" }, _ => new[] { "Series", "Season", "Episode", "Movie" } }; } diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index b012ff42eb..d483ca4d2b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryStructureDto; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 649397d68d..425086895b 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; @@ -143,7 +144,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -547,7 +548,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index b9772a0693..f65d95c411 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 435457af67..69b9042646 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) @@ -150,7 +150,7 @@ public class MusicGenresController : BaseJellyfinApiController MusicGenre? item; - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) { item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 0ba5e995fb..c5e940108c 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 8d2a738d4a..c4c89ccde0 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ParameterObsolete] string? name, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids, [FromQuery, ParameterObsolete] Guid? userId, - [FromQuery, ParameterObsolete] string? mediaType, + [FromQuery, ParameterObsolete] MediaType? mediaType, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) { if (ids.Count == 0) diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 72ad14a281..f63e639276 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 5c77db2407..595cab2df1 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index c8fa11ac62..065466cbca 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 387b3ea5a6..5b45941657 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery, Required] string searchTerm, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e93456de66..fdebb3d450 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; @@ -90,12 +91,6 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => !i.UserId.Equals(default)); } - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - result = result.Where(i => { if (!string.IsNullOrWhiteSpace(i.DeviceId)) @@ -110,6 +105,12 @@ public class SessionController : BaseJellyfinApiController }); } + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + return Ok(result); } @@ -393,7 +394,7 @@ public class SessionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> PostCapabilities( [FromQuery] string? id, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 1098733b2c..41b0858d19 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -3,7 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; -using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Api; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index fb89e96108..49ca058bd4 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.SubtitleDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -115,7 +116,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="200">Subtitles retrieved.</response> /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -135,7 +136,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="204">Subtitle downloaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> DownloadRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -399,7 +400,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="204">Subtitle uploaded.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Videos/{itemId}/Subtitles")] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> UploadSubtitle( [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 5b808f257c..675757fc51 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( [FromRoute, Required] Guid userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 23abba7dc7..3839781971 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 11095a97f0..3d4df03869 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b5b6406206..4fbaafa2a0 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -160,9 +160,9 @@ public class TrailersController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs new file mode 100644 index 0000000000..2dc960229a --- /dev/null +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -0,0 +1,101 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers; + +/// <summary> +/// Trickplay controller. +/// </summary> +[Route("")] +[Authorize] +public class TrickplayController : BaseJellyfinApiController +{ + private readonly ILibraryManager _libraryManager; + private readonly ITrickplayManager _trickplayManager; + + /// <summary> + /// Initializes a new instance of the <see cref="TrickplayController"/> class. + /// </summary> + /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param> + /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param> + public TrickplayController( + ILibraryManager libraryManager, + ITrickplayManager trickplayManager) + { + _libraryManager = libraryManager; + _trickplayManager = trickplayManager; + } + + /// <summary> + /// Gets an image tiles playlist for trickplay. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> + /// <response code="200">Tiles playlist returned.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns> + [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesPlaylistFile] + public async Task<ActionResult> GetTrickplayHlsPlaylist( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int width, + [FromQuery] Guid? mediaSourceId) + { + string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false); + + if (string.IsNullOrEmpty(playlist)) + { + return NotFound(); + } + + return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8); + } + + /// <summary> + /// Gets a trickplay tile image. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="index">The index of the desired tile.</param> + /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> + /// <response code="200">Tile image returned.</response> + /// <response code="200">Tile image not found at specified index.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns> + [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public ActionResult GetTrickplayTileImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int width, + [FromRoute, Required] int index, + [FromQuery] Guid? mediaSourceId) + { + var item = _libraryManager.GetItemById(mediaSourceId ?? itemId); + if (item is null) + { + return NotFound(); + } + + var path = _trickplayManager.GetTrickplayTilePath(item, width, index); + if (System.IO.File.Exists(path)) + { + return PhysicalFile(path, MediaTypeNames.Image.Jpeg); + } + + return NotFound(); + } +} diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index bdbbd1e0db..55a30d4692 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -135,7 +135,7 @@ public class TvShowsController : BaseJellyfinApiController /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> - /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the upcoming episodes.</returns> [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes( @@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy) + [FromQuery] ItemSortBy? sortBy) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) @@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } - if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) + if (sortBy == ItemSortBy.Random) { episodes.Shuffle(); } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 1be40111dd..f9f27f1480 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 838b432340..0ffa3ab1ad 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -63,7 +64,7 @@ public class UserViewsController : BaseJellyfinApiController public QueryResult<BaseItemDto> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, [FromQuery] bool includeHidden = false) { var query = new UserViewQuery diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index c0ec646eda..5d9868eb9f 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -12,11 +12,10 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -41,11 +40,9 @@ public class VideosController : BaseJellyfinApiController private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; - private readonly IDlnaManager _dlnaManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; private readonly EncodingHelper _encodingHelper; @@ -58,11 +55,9 @@ public class VideosController : BaseJellyfinApiController /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> @@ -70,11 +65,9 @@ public class VideosController : BaseJellyfinApiController ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService, - IDlnaManager dlnaManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, IHttpClientFactory httpClientFactory, EncodingHelper encodingHelper) @@ -82,11 +75,9 @@ public class VideosController : BaseJellyfinApiController _libraryManager = libraryManager; _userManager = userManager; _dtoService = dtoService; - _dlnaManager = dlnaManager; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; _encodingHelper = encodingHelper; @@ -323,7 +314,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, + [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, @@ -380,7 +371,6 @@ public class VideosController : BaseJellyfinApiController Static = @static ?? false, Params = @params, Tag = tag, - DeviceProfileId = deviceProfileId, PlaySessionId = playSessionId, SegmentContainer = segmentContainer, SegmentLength = segmentLength, @@ -437,8 +427,6 @@ public class VideosController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _dlnaManager, - _deviceManager, _transcodingJobHelper, _transcodingJobType, cancellationTokenSource.Token) @@ -446,8 +434,6 @@ public class VideosController : BaseJellyfinApiController if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); - var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo is null) { @@ -462,8 +448,6 @@ public class VideosController : BaseJellyfinApiController // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); - var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false); } @@ -474,12 +458,6 @@ public class VideosController : BaseJellyfinApiController } var outputPath = state.OutputFilePath; - var outputPathExists = System.IO.File.Exists(outputPath); - - var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); - var isTranscodeCached = outputPathExists && transcodingJob is not null; - - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 74370db50b..ca46c38c59 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -76,8 +76,8 @@ public class YearsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController return _dtoService.GetBaseItemDto(item, dtoOptions); } - private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes) + private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<MediaType> mediaTypes) { var baseItemKind = f.GetBaseItemKind(); // Exclude item types @@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController } // Include MediaTypes - if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType)) { return false; } |
