From a41d5fcea4ee082bb49ddac34a1606204e12e8e8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 17:36:05 -0600 Subject: [PATCH 01/54] Move AttachmentsService to AttachmentsController --- .../Controllers/AttachmentsController.cs | 86 +++++++++++++++++++ .../Attachments/AttachmentService.cs | 63 -------------- MediaBrowser.Api/MediaBrowser.Api.csproj | 4 + 3 files changed, 90 insertions(+), 63 deletions(-) create mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs delete mode 100644 MediaBrowser.Api/Attachments/AttachmentService.cs diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs new file mode 100644 index 0000000000..5d48a79b9b --- /dev/null +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Attachments controller. + /// + [Route("Videos")] + [Authenticated] + public class AttachmentsController : Controller + { + private readonly ILibraryManager _libraryManager; + private readonly IAttachmentExtractor _attachmentExtractor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public AttachmentsController( + ILibraryManager libraryManager, + IAttachmentExtractor attachmentExtractor) + { + _libraryManager = libraryManager; + _attachmentExtractor = attachmentExtractor; + } + + /// + /// Get video attachment. + /// + /// Video ID. + /// Media Source ID. + /// Attachment Index. + /// Attachment. + [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] + [Produces("application/octet-stream")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task GetAttachment( + [FromRoute] Guid videoId, + [FromRoute] string mediaSourceId, + [FromRoute] int index) + { + try + { + var item = _libraryManager.GetItemById(videoId); + if (item == null) + { + return NotFound(); + } + + var (attachment, stream) = await _attachmentExtractor.GetAttachment( + item, + mediaSourceId, + index, + CancellationToken.None) + .ConfigureAwait(false); + + var contentType = "application/octet-stream"; + if (string.IsNullOrWhiteSpace(attachment.MimeType)) + { + contentType = attachment.MimeType; + } + + return new FileStreamResult(stream, contentType); + } + catch (ResourceNotFoundException e) + { + return StatusCode(StatusCodes.Status404NotFound, e.Message); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + } +} diff --git a/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs deleted file mode 100644 index 1632ca1b06..0000000000 --- a/MediaBrowser.Api/Attachments/AttachmentService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Attachments -{ - [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")] - public class GetAttachment - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - - [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string MediaSourceId { get; set; } - - [ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Index { get; set; } - } - - public class AttachmentService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly IAttachmentExtractor _attachmentExtractor; - - public AttachmentService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - IAttachmentExtractor attachmentExtractor) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _attachmentExtractor = attachmentExtractor; - } - - public async Task Get(GetAttachment request) - { - var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false); - var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType; - - return ResultFactory.GetResult(Request, attachmentStream, mime); - } - - private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request) - { - var item = _libraryManager.GetItemById(request.Id); - - return _attachmentExtractor.GetAttachment(item, - request.MediaSourceId, - request.Index, - CancellationToken.None); - } - } -} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 0d62cf8c59..5ca74d4238 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -9,6 +9,10 @@ + + + + netstandard2.1 false From 1fc682541050e074227736f0c8556d53f98228a1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 17:37:15 -0600 Subject: [PATCH 02/54] nullable --- Jellyfin.Api/Controllers/AttachmentsController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index 5d48a79b9b..f4c1a761fb 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Threading; using System.Threading.Tasks; From 21b54b4ad8477d654e4f79e9805701c9737346a6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 19:33:55 -0600 Subject: [PATCH 03/54] Move DeviceService to DevicesController --- Jellyfin.Api/Controllers/DevicesController.cs | 260 ++++++++++++++++++ MediaBrowser.Api/Devices/DeviceService.cs | 168 ----------- 2 files changed, 260 insertions(+), 168 deletions(-) create mode 100644 Jellyfin.Api/Controllers/DevicesController.cs delete mode 100644 MediaBrowser.Api/Devices/DeviceService.cs diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs new file mode 100644 index 0000000000..7407c44878 --- /dev/null +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -0,0 +1,260 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Devices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Devices Controller. + /// + [Authenticated] + public class DevicesController : BaseJellyfinApiController + { + private readonly IDeviceManager _deviceManager; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public DevicesController( + IDeviceManager deviceManager, + IAuthenticationRepository authenticationRepository, + ISessionManager sessionManager) + { + _deviceManager = deviceManager; + _authenticationRepository = authenticationRepository; + _sessionManager = sessionManager; + } + + /// + /// Get Devices. + /// + /// /// Gets or sets a value indicating whether [supports synchronize]. + /// /// Gets or sets the user identifier. + /// Device Infos. + [HttpGet] + [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + { + try + { + var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; + var devices = _deviceManager.GetDevices(deviceQuery); + return Ok(devices); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get info for a device. + /// + /// Device Id. + /// Device Info. + [HttpGet("Info")] + [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) + { + try + { + var deviceInfo = _deviceManager.GetDevice(id); + if (deviceInfo == null) + { + return NotFound(); + } + + return Ok(deviceInfo); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get options for a device. + /// + /// Device Id. + /// Device Info. + [HttpGet("Options")] + [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + { + try + { + var deviceInfo = _deviceManager.GetDeviceOptions(id); + if (deviceInfo == null) + { + return NotFound(); + } + + return Ok(deviceInfo); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Update device options. + /// + /// Device Id. + /// Device Options. + /// Status. + [HttpPost("Options")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult UpdateDeviceOptions( + [FromQuery, BindRequired] string id, + [FromBody, BindRequired] DeviceOptions deviceOptions) + { + try + { + var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); + if (existingDeviceOptions == null) + { + return NotFound(); + } + + _deviceManager.UpdateDeviceOptions(id, deviceOptions); + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Deletes a device. + /// + /// Device Id. + /// Status. + [HttpDelete] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult DeleteDevice([FromQuery, BindRequired] string id) + { + try + { + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; + + foreach (var session in sessions) + { + _sessionManager.Logout(session); + } + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Gets camera upload history for a device. + /// + /// Device Id. + /// Content Upload History. + [HttpGet("CameraUploads")] + [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) + { + try + { + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); + return Ok(uploadHistory); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Uploads content. + /// + /// Device Id. + /// Album. + /// Name. + /// Id. + /// Status. + [HttpPost("CameraUploads")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task PostCameraUploadAsync( + [FromQuery, BindRequired] string deviceId, + [FromQuery, BindRequired] string album, + [FromQuery, BindRequired] string name, + [FromQuery, BindRequired] string id) + { + try + { + Stream fileStream; + string contentType; + + if (Request.HasFormContentType) + { + if (Request.Form.Files.Any()) + { + fileStream = Request.Form.Files[0].OpenReadStream(); + contentType = Request.Form.Files[0].ContentType; + } + else + { + return BadRequest(); + } + } + else + { + fileStream = Request.Body; + contentType = Request.ContentType; + } + + await _deviceManager.AcceptCameraUpload( + deviceId, + fileStream, + new LocalFileInfo + { + MimeType = contentType, + Album = album, + Name = name, + Id = id + }).ConfigureAwait(false); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + } +} diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs deleted file mode 100644 index 7004a2559e..0000000000 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Devices -{ - [Route("/Devices", "GET", Summary = "Gets all devices")] - [Authenticated(Roles = "Admin")] - public class GetDevices : DeviceQuery, IReturn> - { - } - - [Route("/Devices/Info", "GET", Summary = "Gets info for a device")] - [Authenticated(Roles = "Admin")] - public class GetDeviceInfo : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Devices/Options", "GET", Summary = "Gets options for a device")] - [Authenticated(Roles = "Admin")] - public class GetDeviceOptions : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Devices", "DELETE", Summary = "Deletes a device")] - public class DeleteDevice - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Devices/CameraUploads", "GET", Summary = "Gets camera upload history for a device")] - [Authenticated] - public class GetCameraUploads : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string DeviceId { get; set; } - } - - [Route("/Devices/CameraUploads", "POST", Summary = "Uploads content")] - [Authenticated] - public class PostCameraUpload : IRequiresRequestStream, IReturnVoid - { - [ApiMember(Name = "DeviceId", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string DeviceId { get; set; } - - [ApiMember(Name = "Album", Description = "Album", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Album { get; set; } - - [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } - - [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Devices/Options", "POST", Summary = "Updates device options")] - [Authenticated(Roles = "Admin")] - public class PostDeviceOptions : DeviceOptions, IReturnVoid - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string Id { get; set; } - } - - public class DeviceService : BaseApiService - { - private readonly IDeviceManager _deviceManager; - private readonly IAuthenticationRepository _authRepo; - private readonly ISessionManager _sessionManager; - - public DeviceService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDeviceManager deviceManager, - IAuthenticationRepository authRepo, - ISessionManager sessionManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _deviceManager = deviceManager; - _authRepo = authRepo; - _sessionManager = sessionManager; - } - - public void Post(PostDeviceOptions request) - { - _deviceManager.UpdateDeviceOptions(request.Id, request); - } - - public object Get(GetDevices request) - { - return ToOptimizedResult(_deviceManager.GetDevices(request)); - } - - public object Get(GetDeviceInfo request) - { - return _deviceManager.GetDevice(request.Id); - } - - public object Get(GetDeviceOptions request) - { - return _deviceManager.GetDeviceOptions(request.Id); - } - - public object Get(GetCameraUploads request) - { - return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId)); - } - - public void Delete(DeleteDevice request) - { - var sessions = _authRepo.Get(new AuthenticationInfoQuery - { - DeviceId = request.Id - - }).Items; - - foreach (var session in sessions) - { - _sessionManager.Logout(session); - } - } - - public Task Post(PostCameraUpload request) - { - var deviceId = Request.QueryString["DeviceId"]; - var album = Request.QueryString["Album"]; - var id = Request.QueryString["Id"]; - var name = Request.QueryString["Name"]; - var req = Request.Response.HttpContext.Request; - - if (req.HasFormContentType) - { - var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo - { - MimeType = file.ContentType, - Album = album, - Name = name, - Id = id - }); - } - - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } - } -} From 440f060da6cfa8336d51bd05b723d67cfcf168eb Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 19:36:18 -0600 Subject: [PATCH 04/54] Fix Authenticated Roles --- Jellyfin.Api/Controllers/DevicesController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 7407c44878..a9dcfb955a 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -48,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets or sets the user identifier. /// Device Infos. [HttpGet] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) @@ -70,6 +71,7 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Device Info. [HttpGet("Info")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] @@ -97,6 +99,7 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Device Info. [HttpGet("Options")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] @@ -125,6 +128,7 @@ namespace Jellyfin.Api.Controllers /// Device Options. /// Status. [HttpPost("Options")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] From 08eba82bb7bebe277f6b106fa48994bb98c3dd41 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:52:33 -0600 Subject: [PATCH 05/54] Remove exception handler --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index f4c1a761fb..aeeaf5cbdc 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -79,10 +79,6 @@ namespace Jellyfin.Api.Controllers { return StatusCode(StatusCodes.Status404NotFound, e.Message); } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } } } } From 5ef71d592b84b73290e3e7a34cd7fa8b9f337f50 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:55:01 -0600 Subject: [PATCH 06/54] Remove exception handler --- Jellyfin.Api/Controllers/DevicesController.cs | 143 ++++++------------ 1 file changed, 44 insertions(+), 99 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index a9dcfb955a..5dc3f27ee1 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -53,16 +53,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { - try - { - var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - var devices = _deviceManager.GetDevices(deviceQuery); - return Ok(devices); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; + var devices = _deviceManager.GetDevices(deviceQuery); + return Ok(devices); } /// @@ -77,20 +70,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) { - try - { - var deviceInfo = _deviceManager.GetDevice(id); - if (deviceInfo == null) - { - return NotFound(); - } - - return Ok(deviceInfo); - } - catch (Exception e) + var deviceInfo = _deviceManager.GetDevice(id); + if (deviceInfo == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + return Ok(deviceInfo); } /// @@ -105,20 +91,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { - try + var deviceInfo = _deviceManager.GetDeviceOptions(id); + if (deviceInfo == null) { - var deviceInfo = _deviceManager.GetDeviceOptions(id); - if (deviceInfo == null) - { - return NotFound(); - } - - return Ok(deviceInfo); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + return Ok(deviceInfo); } /// @@ -136,21 +115,14 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string id, [FromBody, BindRequired] DeviceOptions deviceOptions) { - try - { - var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); - if (existingDeviceOptions == null) - { - return NotFound(); - } - - _deviceManager.UpdateDeviceOptions(id, deviceOptions); - return Ok(); - } - catch (Exception e) + var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); + if (existingDeviceOptions == null) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + return NotFound(); } + + _deviceManager.UpdateDeviceOptions(id, deviceOptions); + return Ok(); } /// @@ -163,21 +135,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult DeleteDevice([FromQuery, BindRequired] string id) { - try - { - var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; - - foreach (var session in sessions) - { - _sessionManager.Logout(session); - } + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; - return Ok(); - } - catch (Exception e) + foreach (var session in sessions) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + _sessionManager.Logout(session); } + + return Ok(); } /// @@ -190,15 +155,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) { - try - { - var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return Ok(uploadHistory); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); + return Ok(uploadHistory); } /// @@ -219,46 +177,33 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string name, [FromQuery, BindRequired] string id) { - try - { - Stream fileStream; - string contentType; + Stream fileStream; + string contentType; - if (Request.HasFormContentType) + if (Request.HasFormContentType) + { + if (Request.Form.Files.Any()) { - if (Request.Form.Files.Any()) - { - fileStream = Request.Form.Files[0].OpenReadStream(); - contentType = Request.Form.Files[0].ContentType; - } - else - { - return BadRequest(); - } + fileStream = Request.Form.Files[0].OpenReadStream(); + contentType = Request.Form.Files[0].ContentType; } else { - fileStream = Request.Body; - contentType = Request.ContentType; + return BadRequest(); } - - await _deviceManager.AcceptCameraUpload( - deviceId, - fileStream, - new LocalFileInfo - { - MimeType = contentType, - Album = album, - Name = name, - Id = id - }).ConfigureAwait(false); - - return Ok(); } - catch (Exception e) + else { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + fileStream = Request.Body; + contentType = Request.ContentType; } + + await _deviceManager.AcceptCameraUpload( + deviceId, + fileStream, + new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false); + + return Ok(); } } } From 466e20ea8cb8c262605d06dc01eff4463559d9b0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 13:57:11 -0600 Subject: [PATCH 07/54] move to ActionResult --- Jellyfin.Api/Controllers/AttachmentsController.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index aeeaf5cbdc..351401de18 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -44,10 +44,9 @@ namespace Jellyfin.Api.Controllers /// Attachment. [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] [Produces("application/octet-stream")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task GetAttachment( + public async Task> GetAttachment( [FromRoute] Guid videoId, [FromRoute] string mediaSourceId, [FromRoute] int index) From 927696c4036960018864864a4acbf0aeb797f7ac Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 13:59:43 -0600 Subject: [PATCH 08/54] move to ActionResult --- Jellyfin.Api/Controllers/DevicesController.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 5dc3f27ee1..559a260071 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -49,9 +49,8 @@ namespace Jellyfin.Api.Controllers /// Device Infos. [HttpGet] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; var devices = _deviceManager.GetDevices(deviceQuery); @@ -65,10 +64,9 @@ namespace Jellyfin.Api.Controllers /// Device Info. [HttpGet("Info")] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) + public ActionResult GetDeviceInfo([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -86,10 +84,9 @@ namespace Jellyfin.Api.Controllers /// Device Info. [HttpGet("Options")] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -110,8 +107,7 @@ namespace Jellyfin.Api.Controllers [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateDeviceOptions( + public ActionResult UpdateDeviceOptions( [FromQuery, BindRequired] string id, [FromBody, BindRequired] DeviceOptions deviceOptions) { @@ -132,8 +128,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult DeleteDevice([FromQuery, BindRequired] string id) + public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; @@ -151,9 +146,8 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Content Upload History. [HttpGet("CameraUploads")] - [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { var uploadHistory = _deviceManager.GetCameraUploadHistory(id); return Ok(uploadHistory); @@ -170,8 +164,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task PostCameraUploadAsync( + public async Task PostCameraUploadAsync( [FromQuery, BindRequired] string deviceId, [FromQuery, BindRequired] string album, [FromQuery, BindRequired] string name, From 4d894c4344fd23026bbfdc0a1cdd24231441a444 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:55:47 -0600 Subject: [PATCH 09/54] Remove unneeded Ok calls. --- Jellyfin.Api/Controllers/DevicesController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 559a260071..cebb51ccfe 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(deviceInfo); + return deviceInfo; } /// @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(deviceInfo); + return deviceInfo; } /// @@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return Ok(uploadHistory); + return uploadHistory; } /// From f3da5dc8b7fef7e5fdeddff941c6d99063a1fd97 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:04:37 -0600 Subject: [PATCH 10/54] Fix Authorize attributes --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index 351401de18..b0cdfb86e9 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// [Route("Videos")] - [Authenticated] + [Authorize] public class AttachmentsController : Controller { private readonly ILibraryManager _libraryManager; From c7fe8b04cc854df110528a0eda4383263e99e554 Mon Sep 17 00:00:00 2001 From: Bruce Date: Sat, 25 Apr 2020 19:59:31 +0100 Subject: [PATCH 11/54] PackageService to Jellyfin.API --- Jellyfin.Api/Controllers/PackageController.cs | 115 ++++++++++++ MediaBrowser.Api/PackageService.cs | 171 ------------------ 2 files changed, 115 insertions(+), 171 deletions(-) create mode 100644 Jellyfin.Api/Controllers/PackageController.cs delete mode 100644 MediaBrowser.Api/PackageService.cs diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs new file mode 100644 index 0000000000..1fb9ab697d --- /dev/null +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -0,0 +1,115 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Updates; +using MediaBrowser.Model.Updates; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Package Controller. + /// + [Route("Packages")] + [Authorize] + public class PackageController : BaseJellyfinApiController + { + private readonly IInstallationManager _installationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of Installation Manager. + public PackageController(IInstallationManager installationManager) + { + _installationManager = installationManager; + } + + /// + /// Gets a package, by name or assembly guid. + /// + /// The name of the package. + /// The guid of the associated assembly. + /// Package info. + [HttpGet("/{Name}")] + [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] + public ActionResult GetPackageInfo( + [FromRoute] [Required] string name, + [FromQuery] string? assemblyGuid) + { + var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult(); + var result = _installationManager.FilterPackages( + packages, + name, + string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault(); + + return Ok(result); + } + + /// + /// Gets available packages. + /// + /// Packages information. + [HttpGet] + [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] + public async Task> GetPackages() + { + IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + + return Ok(packages.ToArray()); + } + + /// + /// Installs a package. + /// + /// Package name. + /// Guid of the associated assembly. + /// Optional version. Defaults to latest version. + /// Status. + [HttpPost("/Installed/{Name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task InstallPackage( + [FromRoute] [Required] string name, + [FromQuery] string assemblyGuid, + [FromQuery] string version) + { + var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + var package = _installationManager.GetCompatibleVersions( + packages, + name, + string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid), + string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault(); + + if (package == null) + { + return NotFound(); + } + + await _installationManager.InstallPackage(package).ConfigureAwait(false); + + return Ok(); + } + + /// + /// Cancels a package installation. + /// + /// Installation Id. + /// Status. + [HttpDelete("/Installing/{id}")] + [Authorize(Policy = Policies.RequiresElevation)] + public IActionResult CancelPackageInstallation( + [FromRoute] [Required] string id) + { + _installationManager.CancelInstallation(new Guid(id)); + + return Ok(); + } + } +} diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs deleted file mode 100644 index 444354a992..0000000000 --- a/MediaBrowser.Api/PackageService.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetPackage - /// - [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")] - [Authenticated] - public class GetPackage : IReturn - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string AssemblyGuid { get; set; } - } - - /// - /// Class GetPackages - /// - [Route("/Packages", "GET", Summary = "Gets available packages")] - [Authenticated] - public class GetPackages : IReturn - { - } - - /// - /// Class InstallPackage - /// - [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")] - [Authenticated(Roles = "Admin")] - public class InstallPackage : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string AssemblyGuid { get; set; } - - /// - /// Gets or sets the version. - /// - /// The version. - [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Version { get; set; } - } - - /// - /// Class CancelPackageInstallation - /// - [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")] - [Authenticated(Roles = "Admin")] - public class CancelPackageInstallation : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class PackageService - /// - public class PackageService : BaseApiService - { - private readonly IInstallationManager _installationManager; - - public PackageService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IInstallationManager installationManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _installationManager = installationManager; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPackage request) - { - var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult(); - var result = _installationManager.FilterPackages( - packages, - request.Name, - string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetPackages request) - { - IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - - return ToOptimizedResult(packages.ToArray()); - } - - /// - /// Posts the specified request. - /// - /// The request. - /// - public async Task Post(InstallPackage request) - { - var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - var package = _installationManager.GetCompatibleVersions( - packages, - request.Name, - string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid), - string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault(); - - if (package == null) - { - throw new ResourceNotFoundException( - string.Format( - CultureInfo.InvariantCulture, - "Package not found: {0}", - request.Name)); - } - - await _installationManager.InstallPackage(package); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(CancelPackageInstallation request) - { - _installationManager.CancelInstallation(new Guid(request.Id)); - } - } -} From f66714561e0fef18ba25c36abdf97ee62ccda007 Mon Sep 17 00:00:00 2001 From: Bruce Coelho Date: Sat, 25 Apr 2020 21:32:49 +0100 Subject: [PATCH 12/54] Update Jellyfin.Api/Controllers/PackageController.cs Applying requested changes to PackageController Co-Authored-By: Cody Robibero --- Jellyfin.Api/Controllers/PackageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 1fb9ab697d..ab4d204583 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers name, string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault(); - return Ok(result); + return result; } /// From 5aced0ea0f4bca17aee392698351d54b0ad50e26 Mon Sep 17 00:00:00 2001 From: Bruce Coelho Date: Sat, 25 Apr 2020 21:41:56 +0100 Subject: [PATCH 13/54] Apply suggestions from code review Co-Authored-By: Cody Robibero --- Jellyfin.Api/Controllers/PackageController.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index ab4d204583..1da5ac0e97 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -39,11 +39,11 @@ namespace Jellyfin.Api.Controllers /// Package info. [HttpGet("/{Name}")] [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] - public ActionResult GetPackageInfo( + public async Task> GetPackageInfo( [FromRoute] [Required] string name, [FromQuery] string? assemblyGuid) { - var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult(); + var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var result = _installationManager.FilterPackages( packages, name, @@ -58,11 +58,11 @@ namespace Jellyfin.Api.Controllers /// Packages information. [HttpGet] [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] - public async Task> GetPackages() + public async Task> GetPackages() { IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - return Ok(packages.ToArray()); + return packages; } /// @@ -75,6 +75,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Installed/{Name}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.RequiresElevation)] public async Task InstallPackage( [FromRoute] [Required] string name, [FromQuery] string assemblyGuid, From 0d8253d8e22d4cf34c58577e7fefb3f5733adedd Mon Sep 17 00:00:00 2001 From: Bruce Date: Fri, 1 May 2020 15:17:40 +0100 Subject: [PATCH 14/54] Updated documentation according to discussion in jellyfin#2872 --- Jellyfin.Api/Controllers/PackageController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 1da5ac0e97..b5ee47ee43 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -32,11 +32,11 @@ namespace Jellyfin.Api.Controllers } /// - /// Gets a package, by name or assembly guid. + /// Gets a package by name or assembly guid. /// /// The name of the package. /// The guid of the associated assembly. - /// Package info. + /// A containing package information. [HttpGet("/{Name}")] [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] public async Task> GetPackageInfo( @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets available packages. /// - /// Packages information. + /// An containing available packages information. [HttpGet] [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] public async Task> GetPackages() @@ -71,7 +71,9 @@ namespace Jellyfin.Api.Controllers /// Package name. /// Guid of the associated assembly. /// Optional version. Defaults to latest version. - /// Status. + /// Package found. + /// Package not found. + /// An on success, or a if the package could not be found. [HttpPost("/Installed/{Name}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -102,7 +104,8 @@ namespace Jellyfin.Api.Controllers /// Cancels a package installation. /// /// Installation Id. - /// Status. + /// Installation cancelled. + /// An on successfully cancelling a package installation. [HttpDelete("/Installing/{id}")] [Authorize(Policy = Policies.RequiresElevation)] public IActionResult CancelPackageInstallation( From 7516e3ebbec82b732e8e4355ae108e7030e1e00e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:12:56 -0600 Subject: [PATCH 15/54] Update endpoint docs --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index b0cdfb86e9..30fb951cf9 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -41,7 +41,9 @@ namespace Jellyfin.Api.Controllers /// Video ID. /// Media Source ID. /// Attachment Index. - /// Attachment. + /// Attachment retrieved. + /// Video or attachment not found. + /// An containing the attachment stream on success, or a if the attachment could not be found. [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] [Produces("application/octet-stream")] [ProducesResponseType(StatusCodes.Status200OK)] From 25002483a3fd7f9d1c79c74338ac18c8eabfb0ed Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:23:02 -0600 Subject: [PATCH 16/54] Update endpoint docs --- Jellyfin.Api/Controllers/DevicesController.cs | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index cebb51ccfe..02cf1bc446 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -46,11 +47,12 @@ namespace Jellyfin.Api.Controllers /// /// /// Gets or sets a value indicating whether [supports synchronize]. /// /// Gets or sets the user identifier. - /// Device Infos. + /// Devices retrieved. + /// An containing the list of devices. [HttpGet] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; var devices = _deviceManager.GetDevices(deviceQuery); @@ -61,7 +63,9 @@ namespace Jellyfin.Api.Controllers /// Get info for a device. /// /// Device Id. - /// Device Info. + /// Device info retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. [HttpGet("Info")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -81,7 +85,9 @@ namespace Jellyfin.Api.Controllers /// Get options for a device. /// /// Device Id. - /// Device Info. + /// Device options retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. [HttpGet("Options")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -102,7 +108,9 @@ namespace Jellyfin.Api.Controllers /// /// Device Id. /// Device Options. - /// Status. + /// Device options updated. + /// Device not found. + /// An on success, or a if the device could not be found. [HttpPost("Options")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -125,11 +133,19 @@ namespace Jellyfin.Api.Controllers /// Deletes a device. /// /// Device Id. - /// Status. + /// Device deleted. + /// Device not found. + /// An on success, or a if the device could not be found. [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; foreach (var session in sessions) @@ -144,11 +160,19 @@ namespace Jellyfin.Api.Controllers /// Gets camera upload history for a device. /// /// Device Id. - /// Content Upload History. + /// Device upload history retrieved. + /// Device not found. + /// An containing the device upload history on success, or a if the device could not be found. [HttpGet("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); return uploadHistory; } @@ -160,7 +184,14 @@ namespace Jellyfin.Api.Controllers /// Album. /// Name. /// Id. - /// Status. + /// Contents uploaded. + /// No uploaded contents. + /// Device not found. + /// + /// An on success, + /// or a if the device could not be found + /// or a if the upload contains no files. + /// [HttpPost("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -170,6 +201,12 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string name, [FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + Stream fileStream; string contentType; From c4f8ba55f2b3424be4a6ff1044d13327fe36b687 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 08:28:02 -0600 Subject: [PATCH 17/54] Rename to AttachmentsController -> VideoAttachmentsController --- ...tachmentsController.cs => VideoAttachmentsController.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename Jellyfin.Api/Controllers/{AttachmentsController.cs => VideoAttachmentsController.cs} (94%) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs similarity index 94% rename from Jellyfin.Api/Controllers/AttachmentsController.cs rename to Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 30fb951cf9..69e8473735 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -17,17 +17,17 @@ namespace Jellyfin.Api.Controllers /// [Route("Videos")] [Authorize] - public class AttachmentsController : Controller + public class VideoAttachmentsController : Controller { private readonly ILibraryManager _libraryManager; private readonly IAttachmentExtractor _attachmentExtractor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - public AttachmentsController( + public VideoAttachmentsController( ILibraryManager libraryManager, IAttachmentExtractor attachmentExtractor) { From 45d750f10657da8f7914999098ecffcdbfedbd2d Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 17:36:05 -0600 Subject: [PATCH 18/54] Move AttachmentsService to AttachmentsController --- .../Controllers/AttachmentsController.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs new file mode 100644 index 0000000000..5d48a79b9b --- /dev/null +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Attachments controller. + /// + [Route("Videos")] + [Authenticated] + public class AttachmentsController : Controller + { + private readonly ILibraryManager _libraryManager; + private readonly IAttachmentExtractor _attachmentExtractor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public AttachmentsController( + ILibraryManager libraryManager, + IAttachmentExtractor attachmentExtractor) + { + _libraryManager = libraryManager; + _attachmentExtractor = attachmentExtractor; + } + + /// + /// Get video attachment. + /// + /// Video ID. + /// Media Source ID. + /// Attachment Index. + /// Attachment. + [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] + [Produces("application/octet-stream")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task GetAttachment( + [FromRoute] Guid videoId, + [FromRoute] string mediaSourceId, + [FromRoute] int index) + { + try + { + var item = _libraryManager.GetItemById(videoId); + if (item == null) + { + return NotFound(); + } + + var (attachment, stream) = await _attachmentExtractor.GetAttachment( + item, + mediaSourceId, + index, + CancellationToken.None) + .ConfigureAwait(false); + + var contentType = "application/octet-stream"; + if (string.IsNullOrWhiteSpace(attachment.MimeType)) + { + contentType = attachment.MimeType; + } + + return new FileStreamResult(stream, contentType); + } + catch (ResourceNotFoundException e) + { + return StatusCode(StatusCodes.Status404NotFound, e.Message); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + } +} From 8eac528815bc7ab673b361f48b90b3b28ccbc070 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 17:37:15 -0600 Subject: [PATCH 19/54] nullable --- Jellyfin.Api/Controllers/AttachmentsController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index 5d48a79b9b..f4c1a761fb 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Threading; using System.Threading.Tasks; From 84fcb4926ccce968a920bed7324ef6f037c4f5e1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:52:33 -0600 Subject: [PATCH 20/54] Remove exception handler --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index f4c1a761fb..aeeaf5cbdc 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -79,10 +79,6 @@ namespace Jellyfin.Api.Controllers { return StatusCode(StatusCodes.Status404NotFound, e.Message); } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } } } } From 15e9fbb923b8aa91692cd9c8c68ec7dde638c1e2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 13:57:11 -0600 Subject: [PATCH 21/54] move to ActionResult --- Jellyfin.Api/Controllers/AttachmentsController.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index aeeaf5cbdc..351401de18 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -44,10 +44,9 @@ namespace Jellyfin.Api.Controllers /// Attachment. [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] [Produces("application/octet-stream")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task GetAttachment( + public async Task> GetAttachment( [FromRoute] Guid videoId, [FromRoute] string mediaSourceId, [FromRoute] int index) From 177339e8d5f3ad9eea6a3d6cd068e58d637e443d Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:04:37 -0600 Subject: [PATCH 22/54] Fix Authorize attributes --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index 351401de18..b0cdfb86e9 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// [Route("Videos")] - [Authenticated] + [Authorize] public class AttachmentsController : Controller { private readonly ILibraryManager _libraryManager; From 26a2bea179b8c2d8b772b714e6296c03b5c1e0d3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:12:56 -0600 Subject: [PATCH 23/54] Update endpoint docs --- Jellyfin.Api/Controllers/AttachmentsController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs index b0cdfb86e9..30fb951cf9 100644 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ b/Jellyfin.Api/Controllers/AttachmentsController.cs @@ -41,7 +41,9 @@ namespace Jellyfin.Api.Controllers /// Video ID. /// Media Source ID. /// Attachment Index. - /// Attachment. + /// Attachment retrieved. + /// Video or attachment not found. + /// An containing the attachment stream on success, or a if the attachment could not be found. [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] [Produces("application/octet-stream")] [ProducesResponseType(StatusCodes.Status200OK)] From a7a725173da0be952e0a7407f9f42f1ea1123f84 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 08:28:02 -0600 Subject: [PATCH 24/54] Rename to AttachmentsController -> VideoAttachmentsController --- .../Controllers/AttachmentsController.cs | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs deleted file mode 100644 index 30fb951cf9..0000000000 --- a/Jellyfin.Api/Controllers/AttachmentsController.cs +++ /dev/null @@ -1,85 +0,0 @@ -#nullable enable - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers -{ - /// - /// Attachments controller. - /// - [Route("Videos")] - [Authorize] - public class AttachmentsController : Controller - { - private readonly ILibraryManager _libraryManager; - private readonly IAttachmentExtractor _attachmentExtractor; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public AttachmentsController( - ILibraryManager libraryManager, - IAttachmentExtractor attachmentExtractor) - { - _libraryManager = libraryManager; - _attachmentExtractor = attachmentExtractor; - } - - /// - /// Get video attachment. - /// - /// Video ID. - /// Media Source ID. - /// Attachment Index. - /// Attachment retrieved. - /// Video or attachment not found. - /// An containing the attachment stream on success, or a if the attachment could not be found. - [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] - [Produces("application/octet-stream")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetAttachment( - [FromRoute] Guid videoId, - [FromRoute] string mediaSourceId, - [FromRoute] int index) - { - try - { - var item = _libraryManager.GetItemById(videoId); - if (item == null) - { - return NotFound(); - } - - var (attachment, stream) = await _attachmentExtractor.GetAttachment( - item, - mediaSourceId, - index, - CancellationToken.None) - .ConfigureAwait(false); - - var contentType = "application/octet-stream"; - if (string.IsNullOrWhiteSpace(attachment.MimeType)) - { - contentType = attachment.MimeType; - } - - return new FileStreamResult(stream, contentType); - } - catch (ResourceNotFoundException e) - { - return StatusCode(StatusCodes.Status404NotFound, e.Message); - } - } - } -} From a11a1934399b8cbce0487ced49d2f8e7065b436a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:04:09 -0600 Subject: [PATCH 25/54] Remove CameraUpload endpoints --- Jellyfin.Api/Controllers/DevicesController.cs | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 02cf1bc446..64dc2322dd 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,9 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -155,85 +152,5 @@ namespace Jellyfin.Api.Controllers return Ok(); } - - /// - /// Gets camera upload history for a device. - /// - /// Device Id. - /// Device upload history retrieved. - /// Device not found. - /// An containing the device upload history on success, or a if the device could not be found. - [HttpGet("CameraUploads")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) - { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice == null) - { - return NotFound(); - } - - var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return uploadHistory; - } - - /// - /// Uploads content. - /// - /// Device Id. - /// Album. - /// Name. - /// Id. - /// Contents uploaded. - /// No uploaded contents. - /// Device not found. - /// - /// An on success, - /// or a if the device could not be found - /// or a if the upload contains no files. - /// - [HttpPost("CameraUploads")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task PostCameraUploadAsync( - [FromQuery, BindRequired] string deviceId, - [FromQuery, BindRequired] string album, - [FromQuery, BindRequired] string name, - [FromQuery, BindRequired] string id) - { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice == null) - { - return NotFound(); - } - - Stream fileStream; - string contentType; - - if (Request.HasFormContentType) - { - if (Request.Form.Files.Any()) - { - fileStream = Request.Form.Files[0].OpenReadStream(); - contentType = Request.Form.Files[0].ContentType; - } - else - { - return BadRequest(); - } - } - else - { - fileStream = Request.Body; - contentType = Request.ContentType; - } - - await _deviceManager.AcceptCameraUpload( - deviceId, - fileStream, - new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false); - - return Ok(); - } } } From cf78edc979b626ff11ff88889f618cba50c5ee5f Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:05:23 -0600 Subject: [PATCH 26/54] Fix Authorize attributes --- Jellyfin.Api/Controllers/DevicesController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 64dc2322dd..b22b5f985b 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -16,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// /// Devices Controller. /// - [Authenticated] + [Authorize] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; @@ -47,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Devices retrieved. /// An containing the list of devices. [HttpGet] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { @@ -64,7 +66,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An containing the device info on success, or a if the device could not be found. [HttpGet("Info")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDeviceInfo([FromQuery, BindRequired] string id) @@ -86,7 +88,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An containing the device info on success, or a if the device could not be found. [HttpGet("Options")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) @@ -109,7 +111,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An on success, or a if the device could not be found. [HttpPost("Options")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( From cdb25e355c6ebf9ba09f44a7bb7e35286e50976e Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:06:25 -0600 Subject: [PATCH 27/54] Fix return value --- Jellyfin.Api/Controllers/DevicesController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index b22b5f985b..a46d3f9370 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -51,11 +52,10 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - var devices = _deviceManager.GetDevices(deviceQuery); - return Ok(devices); + return _deviceManager.GetDevices(deviceQuery); } /// From 24543b04c110b7cdf275c314ac61065dc36b25e8 Mon Sep 17 00:00:00 2001 From: Bruce Date: Tue, 19 May 2020 18:13:42 +0100 Subject: [PATCH 28/54] Applying review suggestion to documentation --- Jellyfin.Api/Controllers/PackageController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index b5ee47ee43..f37319c19e 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -1,4 +1,5 @@ #nullable enable + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -32,10 +33,10 @@ namespace Jellyfin.Api.Controllers } /// - /// Gets a package by name or assembly guid. + /// Gets a package by name or assembly GUID. /// /// The name of the package. - /// The guid of the associated assembly. + /// The GUID of the associated assembly. /// A containing package information. [HttpGet("/{Name}")] [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] @@ -69,7 +70,7 @@ namespace Jellyfin.Api.Controllers /// Installs a package. /// /// Package name. - /// Guid of the associated assembly. + /// GUID of the associated assembly. /// Optional version. Defaults to latest version. /// Package found. /// Package not found. From b28dd47a0fc5b18111678acede335474f9007b8f Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 12:58:09 -0600 Subject: [PATCH 29/54] implement review suggestions --- Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 69e8473735..a10dd40593 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers } catch (ResourceNotFoundException e) { - return StatusCode(StatusCodes.Status404NotFound, e.Message); + return NotFound(e.Message); } } } From 51d54a8ca40f987bce877ad1d7dc78b1cb26b8a3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 13:02:02 -0600 Subject: [PATCH 30/54] Fix return content type --- .../Controllers/VideoAttachmentsController.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index a10dd40593..596d211900 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,11 +1,13 @@ #nullable enable using System; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,7 +19,7 @@ namespace Jellyfin.Api.Controllers /// [Route("Videos")] [Authorize] - public class VideoAttachmentsController : Controller + public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; private readonly IAttachmentExtractor _attachmentExtractor; @@ -45,7 +47,7 @@ namespace Jellyfin.Api.Controllers /// Video or attachment not found. /// An containing the attachment stream on success, or a if the attachment could not be found. [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] - [Produces("application/octet-stream")] + [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetAttachment( @@ -68,11 +70,9 @@ namespace Jellyfin.Api.Controllers CancellationToken.None) .ConfigureAwait(false); - var contentType = "application/octet-stream"; - if (string.IsNullOrWhiteSpace(attachment.MimeType)) - { - contentType = attachment.MimeType; - } + var contentType = string.IsNullOrWhiteSpace(attachment.MimeType) + ? MediaTypeNames.Application.Octet + : attachment.MimeType; return new FileStreamResult(stream, contentType); } From 2689865858c779491c98066df3f1e7d894f7c3b8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 13:02:35 -0600 Subject: [PATCH 31/54] Remove unused using --- Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 596d211900..86d9322fe4 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; From a5a39300bc733ad7b1d3c683f5f290a742171661 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 26 May 2020 16:55:27 +0200 Subject: [PATCH 32/54] Don't send Exception message in Production Environment --- .../Middleware/ExceptionMiddleware.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 0d79bbfaff..dd4d1ee99b 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -7,7 +7,9 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Middleware @@ -20,6 +22,7 @@ namespace Jellyfin.Server.Middleware private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly IServerConfigurationManager _configuration; + private readonly IWebHostEnvironment _hostEnvironment; /// /// Initializes a new instance of the class. @@ -27,14 +30,17 @@ namespace Jellyfin.Server.Middleware /// Next request delegate. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public ExceptionMiddleware( RequestDelegate next, ILogger logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IWebHostEnvironment hostEnvironment) { _next = next; _logger = logger; _configuration = serverConfigurationManager; + _hostEnvironment = hostEnvironment; } /// @@ -85,6 +91,14 @@ namespace Jellyfin.Server.Middleware context.Response.StatusCode = GetStatusCode(ex); context.Response.ContentType = MediaTypeNames.Text.Plain; + + // Don't send exception unless the server is in a Development environment + if (!_hostEnvironment.IsDevelopment()) + { + await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false); + return; + } + var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } From b944b8f8c54963f61eee5eeb97cd1745ae42ac50 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 11:03:08 -0600 Subject: [PATCH 33/54] Enable CORS and Authentication. --- .../ApiServiceCollectionExtensions.cs | 8 ++++- Jellyfin.Server/Models/ServerCorsPolicy.cs | 30 +++++++++++++++++++ Jellyfin.Server/Startup.cs | 4 ++- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server/Models/ServerCorsPolicy.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 344ef6a5ff..239c71503a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; +using Jellyfin.Server.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -71,7 +72,12 @@ namespace Jellyfin.Server.Extensions /// The MVC builder. public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) { - return serviceCollection.AddMvc(opts => + return serviceCollection + .AddCors(options => + { + options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); + }) + .AddMvc(opts => { opts.UseGeneralRoutePrefix(baseUrl); opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs new file mode 100644 index 0000000000..ae010c042e --- /dev/null +++ b/Jellyfin.Server/Models/ServerCorsPolicy.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Jellyfin.Server.Models +{ + /// + /// Server Cors Policy. + /// + public static class ServerCorsPolicy + { + /// + /// Default policy name. + /// + public const string DefaultPolicyName = "DefaultCorsPolicy"; + + /// + /// Default Policy. Allow Everything. + /// + public static readonly CorsPolicy DefaultPolicy = new CorsPolicy + { + // Allow any origin + Origins = { "*" }, + + // Allow any method + Methods = { "*" }, + + // Allow any header + Headers = { "*" } + }; + } +} \ No newline at end of file diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7c49afbfc6..bd2887e4a0 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,5 +1,6 @@ using Jellyfin.Server.Extensions; using Jellyfin.Server.Middleware; +using Jellyfin.Server.Models; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -68,9 +69,10 @@ namespace Jellyfin.Server // TODO app.UseMiddleware(); app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync); - // TODO use when old API is removed: app.UseAuthentication(); + app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); + app.UseCors(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); app.UseEndpoints(endpoints => { From 9f0b5f347a9b7eeff1d0b079ceee42bc781aed7a Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 11:06:15 -0600 Subject: [PATCH 34/54] Switch Config controller to System.Text.Json --- .../Controllers/ConfigurationController.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 992cb00874..2a1dce74d4 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,12 @@ #nullable enable +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers { private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _jsonSerializer; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ConfigurationController( IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder, - IJsonSerializer jsonSerializer) + IMediaEncoder mediaEncoder) { _configurationManager = configurationManager; _mediaEncoder = mediaEncoder; - _jsonSerializer = jsonSerializer; } /// @@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers public async Task UpdateNamedConfiguration([FromRoute] string key) { var configurationType = _configurationManager.GetConfigurationType(key); - /* - // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. - var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); - */ - - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) - .ConfigureAwait(false); + var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); return Ok(); } From e30a85025f3d0f8b936827613239da7c2c2387c2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 12:42:59 -0600 Subject: [PATCH 35/54] Remove log spam when using legacy api --- .../HttpServer/Security/AuthService.cs | 6 ++++++ Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 11 +++++++++-- MediaBrowser.Controller/Net/AuthenticatedAttribute.cs | 4 ++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 58421aaf19..18bea59ad5 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -146,11 +146,17 @@ namespace Emby.Server.Implementations.HttpServer.Security { return true; } + if (authAttribtues.AllowLocalOnly && request.IsLocal) { return true; } + if (authAttribtues.IgnoreLegacyAuth) + { + return true; + } + return false; } diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 26f7d9d2dd..a0c9c3f5aa 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -37,13 +37,20 @@ namespace Jellyfin.Api.Auth /// protected override Task HandleAuthenticateAsync() { - var authenticatedAttribute = new AuthenticatedAttribute(); + var authenticatedAttribute = new AuthenticatedAttribute + { + IgnoreLegacyAuth = true + }; + try { var user = _authService.Authenticate(Request, authenticatedAttribute); if (user == null) { - return Task.FromResult(AuthenticateResult.Fail("Invalid user")); + return Task.FromResult(AuthenticateResult.NoResult()); + // TODO return when legacy API is removed. + // Don't spam the log with "Invalid User" + // return Task.FromResult(AuthenticateResult.Fail("Invalid user")); } var claims = new[] diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs index 29fb81e32a..9f2743ea18 100644 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs @@ -52,6 +52,8 @@ namespace MediaBrowser.Controller.Net return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } + public bool IgnoreLegacyAuth { get; set; } + public bool AllowLocalOnly { get; set; } } @@ -63,5 +65,7 @@ namespace MediaBrowser.Controller.Net bool AllowLocalOnly { get; } string[] GetRoles(); + + bool IgnoreLegacyAuth { get; } } } From 6d9f564a949e326e909cbcfd37d254195b40ba56 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 2 Jun 2020 15:04:55 +0200 Subject: [PATCH 36/54] Remove duplicate code Co-authored-by: Vasily --- Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index dd4d1ee99b..63effafc19 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -93,13 +93,9 @@ namespace Jellyfin.Server.Middleware context.Response.ContentType = MediaTypeNames.Text.Plain; // Don't send exception unless the server is in a Development environment - if (!_hostEnvironment.IsDevelopment()) - { - await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false); - return; - } - - var errorContent = NormalizeExceptionMessage(ex.Message); + var errorContent = _hostEnvironment.IsDevelopment() + ? NormalizeExceptionMessage(ex.Message) + : "Error processing request."; await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } } From 638cfa32abc3ffd61119f774ffc5a0f5a490d3e9 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 2 Jun 2020 15:07:07 +0200 Subject: [PATCH 37/54] Move SearchService to new API endpoint --- Jellyfin.Api/BaseJellyfinApiController.cs | 19 ++ Jellyfin.Api/Controllers/SearchController.cs | 266 +++++++++++++++ MediaBrowser.Api/SearchService.cs | 333 ------------------- 3 files changed, 285 insertions(+), 333 deletions(-) create mode 100644 Jellyfin.Api/Controllers/SearchController.cs delete mode 100644 MediaBrowser.Api/SearchService.cs diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 1f4508e6cb..6a9e48f8dd 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -9,5 +10,23 @@ namespace Jellyfin.Api [Route("[controller]")] public class BaseJellyfinApiController : ControllerBase { + /// + /// Splits a string at a seperating character into an array of substrings. + /// + /// The string to split. + /// The char that seperates the substrings. + /// Option to remove empty substrings from the array. + /// An array of the substrings. + internal static string[] Split(string value, char separator, bool removeEmpty) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty(); + } + + return removeEmpty + ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) + : value.Split(separator); + } } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs new file mode 100644 index 0000000000..15a650bf97 --- /dev/null +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -0,0 +1,266 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Search; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Search controller. + /// + [Route("/Search/Hints")] + [Authenticated] + public class SearchController : BaseJellyfinApiController + { + private readonly ISearchEngine _searchEngine; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SearchController( + ISearchEngine searchEngine, + ILibraryManager libraryManager, + IDtoService dtoService, + IImageProcessor imageProcessor) + { + _searchEngine = searchEngine; + _libraryManager = libraryManager; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + } + + /// + /// Gets the search hint result. + /// + /// The record index to start at. All items with a lower index will be dropped from the results. + /// The maximum number of records to return. + /// Supply a user id to search within a user's library or omit to search all. + /// The search term to filter on. + /// Optional filter whether to include people. + /// Optional filter whether to include media. + /// Optional filter whether to include genres. + /// Optional filter whether to include studios. + /// Optional filter whether to include artists. + /// If specified, only results with the specified item types are returned. This allows multiple, comma delimeted. + /// If specified, results with these item types are filtered out. This allows multiple, comma delimeted. + /// If specified, only results with the specified media types are returned. This allows multiple, comma delimeted. + /// If specified, only children of the parent are returned. + /// Optional filter for movies. + /// Optional filter for series. + /// Optional filter for news. + /// Optional filter for kids. + /// Optional filter for sports. + /// An with the results of the search. + [HttpGet] + [Description("Gets search hints based on a search term")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult Get( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] Guid userId, + [FromQuery, Required] string searchTerm, + [FromQuery] bool includePeople, + [FromQuery] bool includeMedia, + [FromQuery] bool includeGenres, + [FromQuery] bool includeStudios, + [FromQuery] bool includeArtists, + [FromQuery] string includeItemTypes, + [FromQuery] string excludeItemTypes, + [FromQuery] string mediaTypes, + [FromQuery] string parentId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports) + { + var result = _searchEngine.GetSearchHints(new SearchQuery + { + Limit = limit, + SearchTerm = searchTerm, + IncludeArtists = includeArtists, + IncludeGenres = includeGenres, + IncludeMedia = includeMedia, + IncludePeople = includePeople, + IncludeStudios = includeStudios, + StartIndex = startIndex, + UserId = userId, + IncludeItemTypes = Split(includeItemTypes, ',', true), + ExcludeItemTypes = Split(excludeItemTypes, ',', true), + MediaTypes = Split(mediaTypes, ',', true), + ParentId = parentId, + + IsKids = isKids, + IsMovie = isMovie, + IsNews = isNews, + IsSeries = isSeries, + IsSports = isSports + }); + + return Ok(new SearchHintResult + { + TotalRecordCount = result.TotalRecordCount, + SearchHints = result.Items.Select(GetSearchHintResult).ToArray() + }); + } + + /// + /// Gets the search hint result. + /// + /// The hint info. + /// SearchHintResult. + private SearchHint GetSearchHintResult(SearchHintInfo hintInfo) + { + var item = hintInfo.Item; + + var result = new SearchHint + { + Name = item.Name, + IndexNumber = item.IndexNumber, + ParentIndexNumber = item.ParentIndexNumber, + Id = item.Id, + Type = item.GetClientTypeName(), + MediaType = item.MediaType, + MatchedTerm = hintInfo.MatchedTerm, + RunTimeTicks = item.RunTimeTicks, + ProductionYear = item.ProductionYear, + ChannelId = item.ChannelId, + EndDate = item.EndDate + }; + + // legacy + result.ItemId = result.Id; + + if (item.IsFolder) + { + result.IsFolder = true; + } + + var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary); + + if (primaryImageTag != null) + { + result.PrimaryImageTag = primaryImageTag; + result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item); + } + + SetThumbImageInfo(result, item); + SetBackdropImageInfo(result, item); + + switch (item) + { + case IHasSeries hasSeries: + result.Series = hasSeries.SeriesName; + break; + case LiveTvProgram program: + result.StartDate = program.StartDate; + break; + case Series series: + if (series.Status.HasValue) + { + result.Status = series.Status.Value.ToString(); + } + + break; + case MusicAlbum album: + result.Artists = album.Artists; + result.AlbumArtist = album.AlbumArtist; + break; + case Audio song: + result.AlbumArtist = song.AlbumArtists?[0]; + result.Artists = song.Artists; + + MusicAlbum musicAlbum = song.AlbumEntity; + + if (musicAlbum != null) + { + result.Album = musicAlbum.Name; + result.AlbumId = musicAlbum.Id; + } + else + { + result.Album = song.Album; + } + + break; + } + + if (!item.ChannelId.Equals(Guid.Empty)) + { + var channel = _libraryManager.GetItemById(item.ChannelId); + result.ChannelName = channel?.Name; + } + + return result; + } + + private void SetThumbImageInfo(SearchHint hint, BaseItem item) + { + var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; + + if (itemWithImage == null && item is Episode) + { + itemWithImage = GetParentWithImage(item, ImageType.Thumb); + } + + if (itemWithImage == null) + { + itemWithImage = GetParentWithImage(item, ImageType.Thumb); + } + + if (itemWithImage != null) + { + var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb); + + if (tag != null) + { + hint.ThumbImageTag = tag; + hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + } + } + } + + private void SetBackdropImageInfo(SearchHint hint, BaseItem item) + { + var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) + ?? GetParentWithImage(item, ImageType.Backdrop); + + if (itemWithImage != null) + { + var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop); + + if (tag != null) + { + hint.BackdropImageTag = tag; + hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + } + } + } + + private T GetParentWithImage(BaseItem item, ImageType type) + where T : BaseItem + { + return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type)); + } + } +} diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs deleted file mode 100644 index e9d339c6e3..0000000000 --- a/MediaBrowser.Api/SearchService.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Search; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetSearchHints - /// - [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")] - public class GetSearchHints : IReturn - { - /// - /// Skips over a given number of items within the results. Use for paging. - /// - /// The start index. - [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? StartIndex { get; set; } - - /// - /// The maximum number of items to return - /// - /// The limit. - [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Limit { get; set; } - - /// - /// Gets or sets the user id. - /// - /// The user id. - [ApiMember(Name = "UserId", Description = "Optional. Supply a user id to search within a user's library or omit to search all.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - - /// - /// Search characters used to find items - /// - /// The index by. - [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string SearchTerm { get; set; } - - - [ApiMember(Name = "IncludePeople", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludePeople { get; set; } - - [ApiMember(Name = "IncludeMedia", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeMedia { get; set; } - - [ApiMember(Name = "IncludeGenres", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeGenres { get; set; } - - [ApiMember(Name = "IncludeStudios", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeStudios { get; set; } - - [ApiMember(Name = "IncludeArtists", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool IncludeArtists { get; set; } - - [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string IncludeItemTypes { get; set; } - - [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string ExcludeItemTypes { get; set; } - - [ApiMember(Name = "MediaTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string MediaTypes { get; set; } - - public string ParentId { get; set; } - - [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] - public bool? IsMovie { get; set; } - - [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] - public bool? IsSeries { get; set; } - - [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] - public bool? IsNews { get; set; } - - [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] - public bool? IsKids { get; set; } - - [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] - public bool? IsSports { get; set; } - - public GetSearchHints() - { - IncludeArtists = true; - IncludeGenres = true; - IncludeMedia = true; - IncludePeople = true; - IncludeStudios = true; - } - } - - /// - /// Class SearchService - /// - [Authenticated] - public class SearchService : BaseApiService - { - /// - /// The _search engine - /// - private readonly ISearchEngine _searchEngine; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IImageProcessor _imageProcessor; - - /// - /// Initializes a new instance of the class. - /// - /// The search engine. - /// The library manager. - /// The dto service. - /// The image processor. - public SearchService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISearchEngine searchEngine, - ILibraryManager libraryManager, - IDtoService dtoService, - IImageProcessor imageProcessor) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _searchEngine = searchEngine; - _libraryManager = libraryManager; - _dtoService = dtoService; - _imageProcessor = imageProcessor; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetSearchHints request) - { - var result = GetSearchHintsAsync(request); - - return ToOptimizedResult(result); - } - - /// - /// Gets the search hints async. - /// - /// The request. - /// Task{IEnumerable{SearchHintResult}}. - private SearchHintResult GetSearchHintsAsync(GetSearchHints request) - { - var result = _searchEngine.GetSearchHints(new SearchQuery - { - Limit = request.Limit, - SearchTerm = request.SearchTerm, - IncludeArtists = request.IncludeArtists, - IncludeGenres = request.IncludeGenres, - IncludeMedia = request.IncludeMedia, - IncludePeople = request.IncludePeople, - IncludeStudios = request.IncludeStudios, - StartIndex = request.StartIndex, - UserId = request.UserId, - IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true), - ExcludeItemTypes = ApiEntryPoint.Split(request.ExcludeItemTypes, ',', true), - MediaTypes = ApiEntryPoint.Split(request.MediaTypes, ',', true), - ParentId = request.ParentId, - - IsKids = request.IsKids, - IsMovie = request.IsMovie, - IsNews = request.IsNews, - IsSeries = request.IsSeries, - IsSports = request.IsSports - - }); - - return new SearchHintResult - { - TotalRecordCount = result.TotalRecordCount, - - SearchHints = result.Items.Select(GetSearchHintResult).ToArray() - }; - } - - /// - /// Gets the search hint result. - /// - /// The hint info. - /// SearchHintResult. - private SearchHint GetSearchHintResult(SearchHintInfo hintInfo) - { - var item = hintInfo.Item; - - var result = new SearchHint - { - Name = item.Name, - IndexNumber = item.IndexNumber, - ParentIndexNumber = item.ParentIndexNumber, - Id = item.Id, - Type = item.GetClientTypeName(), - MediaType = item.MediaType, - MatchedTerm = hintInfo.MatchedTerm, - RunTimeTicks = item.RunTimeTicks, - ProductionYear = item.ProductionYear, - ChannelId = item.ChannelId, - EndDate = item.EndDate - }; - - // legacy - result.ItemId = result.Id; - - if (item.IsFolder) - { - result.IsFolder = true; - } - - var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary); - - if (primaryImageTag != null) - { - result.PrimaryImageTag = primaryImageTag; - result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item); - } - - SetThumbImageInfo(result, item); - SetBackdropImageInfo(result, item); - - switch (item) - { - case IHasSeries hasSeries: - result.Series = hasSeries.SeriesName; - break; - case LiveTvProgram program: - result.StartDate = program.StartDate; - break; - case Series series: - if (series.Status.HasValue) - { - result.Status = series.Status.Value.ToString(); - } - - break; - case MusicAlbum album: - result.Artists = album.Artists; - result.AlbumArtist = album.AlbumArtist; - break; - case Audio song: - result.AlbumArtist = song.AlbumArtists.FirstOrDefault(); - result.Artists = song.Artists; - - MusicAlbum musicAlbum = song.AlbumEntity; - - if (musicAlbum != null) - { - result.Album = musicAlbum.Name; - result.AlbumId = musicAlbum.Id; - } - else - { - result.Album = song.Album; - } - - break; - } - - if (!item.ChannelId.Equals(Guid.Empty)) - { - var channel = _libraryManager.GetItemById(item.ChannelId); - result.ChannelName = channel?.Name; - } - - return result; - } - - private void SetThumbImageInfo(SearchHint hint, BaseItem item) - { - var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; - - if (itemWithImage == null && item is Episode) - { - itemWithImage = GetParentWithImage(item, ImageType.Thumb); - } - - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage(item, ImageType.Thumb); - } - - if (itemWithImage != null) - { - var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb); - - if (tag != null) - { - hint.ThumbImageTag = tag; - hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); - } - } - } - - private void SetBackdropImageInfo(SearchHint hint, BaseItem item) - { - var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) - ?? GetParentWithImage(item, ImageType.Backdrop); - - if (itemWithImage != null) - { - var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop); - - if (tag != null) - { - hint.BackdropImageTag = tag; - hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); - } - } - } - - private T GetParentWithImage(BaseItem item, ImageType type) - where T : BaseItem - { - return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type)); - } - } -} From 4fe0beec162d4554f1d6cc3c658b672eafbfa307 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 21 May 2020 08:44:15 -0600 Subject: [PATCH 38/54] Fix Json Enum conversion, map all JsonDefaults properties to API --- .../ApiServiceCollectionExtensions.cs | 22 +++++++--- .../CamelCaseJsonProfileFormatter.cs | 4 +- .../PascalCaseJsonProfileFormatter.cs | 4 +- Jellyfin.Server/Models/JsonOptions.cs | 41 ------------------- MediaBrowser.Common/Json/JsonDefaults.cs | 34 ++++++++++++++- 5 files changed, 53 insertions(+), 52 deletions(-) delete mode 100644 Jellyfin.Server/Models/JsonOptions.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 344ef6a5ff..b9f55e2008 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; -using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; @@ -11,6 +8,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -83,8 +81,20 @@ namespace Jellyfin.Server.Extensions .AddApplicationPart(typeof(StartupController).Assembly) .AddJsonOptions(options => { - // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. - options.JsonSerializerOptions.PropertyNamingPolicy = null; + // Update all properties that are set in JsonDefaults + var jsonOptions = JsonDefaults.PascalCase; + + // From JsonDefaults + options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; + options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; + options.JsonSerializerOptions.Converters.Clear(); + foreach (var converter in jsonOptions.Converters) + { + options.JsonSerializerOptions.Converters.Add(converter); + } + + // From JsonDefaults.PascalCase + options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; }) .AddControllersAsServices(); } @@ -98,7 +108,7 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); + c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index e6ad6dfb13..989c8ecea2 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase) + public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase) { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 675f4c79ee..69963b3fb3 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) + public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs deleted file mode 100644 index 2f0df3d2c7..0000000000 --- a/Jellyfin.Server/Models/JsonOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json; - -namespace Jellyfin.Server.Models -{ - /// - /// Json Options. - /// - public static class JsonOptions - { - /// - /// Gets CamelCase json options. - /// - public static JsonSerializerOptions CamelCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - return options; - } - } - - /// - /// Gets PascalCase json options. - /// - public static JsonSerializerOptions PascalCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = null; - return options; - } - } - - /// - /// Gets base Json Serializer Options. - /// - private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); - } -} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 4a6ee0a793..326f04eea1 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json /// /// Gets the default options. /// + /// + /// When changing these options, update + /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs + /// -> AddJellyfinApi + /// -> AddJsonOptions + /// /// The default options. public static JsonSerializerOptions GetOptions() { - var options = new JsonSerializerOptions() + var options = new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Disallow, WriteIndented = false @@ -26,5 +32,31 @@ namespace MediaBrowser.Common.Json return options; } + + /// + /// Gets CamelCase json options. + /// + public static JsonSerializerOptions CamelCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// + /// Gets PascalCase json options. + /// + public static JsonSerializerOptions PascalCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = null; + return options; + } + } } } From 0e41c4727d84edbf4d7b96c59e0a3d3bec87b633 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 10:36:32 -0600 Subject: [PATCH 39/54] revert to System.Text.JsonSerializer --- .../Controllers/ConfigurationController.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 992cb00874..8243bfce4c 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,12 @@ #nullable enable +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers { private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _jsonSerializer; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ConfigurationController( IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder, - IJsonSerializer jsonSerializer) + IMediaEncoder mediaEncoder) { _configurationManager = configurationManager; _mediaEncoder = mediaEncoder; - _jsonSerializer = jsonSerializer; } /// @@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers public async Task UpdateNamedConfiguration([FromRoute] string key) { var configurationType = _configurationManager.GetConfigurationType(key); - /* - // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); - */ - - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) - .ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); return Ok(); } From cf9cbfff5667961eebec04f20c844f9df988c5a7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 2 Jun 2020 08:28:37 -0600 Subject: [PATCH 40/54] Add second endpoint for Startup/User --- Jellyfin.Api/Controllers/StartupController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index ed1dc1ede3..57a02e62a9 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -109,6 +109,7 @@ namespace Jellyfin.Api.Controllers /// Initial user retrieved. /// The first user. [HttpGet("User")] + [HttpGet("FirstUser")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetFirstUser() { From 2476848dd3d69252c5bc8b45a4eddf545354a0c0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 2 Jun 2020 10:12:55 -0600 Subject: [PATCH 41/54] Fix tests --- .../Auth/CustomAuthenticationHandlerTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 3b3d03c8b7..437dfa410b 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -88,7 +88,9 @@ namespace Jellyfin.Api.Tests.Auth var authenticateResult = await _sut.AuthenticateAsync(); Assert.False(authenticateResult.Succeeded); - Assert.Equal("Invalid user", authenticateResult.Failure.Message); + Assert.True(authenticateResult.None); + // TODO return when legacy API is removed. + // Assert.Equal("Invalid user", authenticateResult.Failure.Message); } [Fact] From 01a5103fef83bbbef230faf2303d16648981a5d2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 2 Jun 2020 11:47:00 -0600 Subject: [PATCH 42/54] Add Dictionary with non-string keys to System.Text.Json --- .../ApiServiceCollectionExtensions.cs | 26 ++++++ .../JsonNonStringKeyDictionaryConverter.cs | 79 +++++++++++++++++++ ...nNonStringKeyDictionaryConverterFactory.cs | 60 ++++++++++++++ MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 4 files changed, 166 insertions(+) create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b9f55e2008..cb4189587d 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Jellyfin.Api; using Jellyfin.Api.Auth; @@ -9,6 +11,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; using MediaBrowser.Common.Json; +using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -128,7 +131,30 @@ namespace Jellyfin.Server.Extensions // Use method name as operationId c.CustomOperationIds(description => description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); + + // TODO - remove when all types are supported in System.Text.Json + c.AddSwaggerTypeMappings(); }); } + + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) + { + /* + * TODO remove when System.Text.Json supports non-string keys. + * Used in Jellyfin.Api.Controller.GetChannels. + */ + options.MapType>(() => + new OpenApiSchema + { + Type = "object", + Properties = typeof(ImageType).GetEnumNames().ToDictionary( + name => name, + name => new OpenApiSchema + { + Type = "string", + Format = "string" + }) + }); + } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs new file mode 100644 index 0000000000..f2ddd7fea2 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -0,0 +1,79 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Converter for Dictionaries without string key. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + /// Type of key. + /// Type of value. + internal sealed class JsonNonStringKeyDictionaryConverter : JsonConverter> + { + /// + /// Read JSON. + /// + /// The Utf8JsonReader. + /// The type to convert. + /// The json serializer options. + /// Typed dictionary. + /// + public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]); + var value = JsonSerializer.Deserialize(ref reader, convertedType, options); + var instance = (Dictionary)Activator.CreateInstance( + typeToConvert, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null); + var parse = typeof(TKey).GetMethod( + "Parse", + 0, + BindingFlags.Public | BindingFlags.Static, + null, + CallingConventions.Any, + new[] { typeof(string) }, + null); + if (parse == null) + { + throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary is not supported."); + } + + while (enumerator.MoveNext()) + { + var element = (KeyValuePair)enumerator.Current; + instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value); + } + + return instance; + } + + /// + /// Write dictionary as Json. + /// + /// The Utf8JsonWriter. + /// The dictionary value. + /// The Json serializer options. + public override void Write(Utf8JsonWriter writer, IDictionary value, JsonSerializerOptions options) + { + var convertedDictionary = new Dictionary(value.Count); + foreach (var (k, v) in value) + { + convertedDictionary[k?.ToString()] = v; + } + JsonSerializer.Serialize(writer, convertedDictionary, options); + convertedDictionary.Clear(); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs new file mode 100644 index 0000000000..d9795a189a --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs @@ -0,0 +1,60 @@ +#nullable enable + +using System; +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory + { + /// + /// Only convert objects that implement IDictionary and do not have string keys. + /// + /// Type convert. + /// Conversion ability. + public override bool CanConvert(Type typeToConvert) + { + + if (!typeToConvert.IsGenericType) + { + return false; + } + + // Let built in converter handle string keys + if (typeToConvert.GenericTypeArguments[0] == typeof(string)) + { + return false; + } + + // Only support objects that implement IDictionary + return typeToConvert.GetInterface(nameof(IDictionary)) != null; + } + + /// + /// Create converter for generic dictionary type. + /// + /// Type to convert. + /// Json serializer options. + /// JsonConverter for given type. + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>) + .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]); + var converter = (JsonConverter)Activator.CreateInstance( + converterType, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + return converter; + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 326f04eea1..f38e2893ec 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -29,6 +29,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); return options; } From 8b59934ccb53fda0ccfda2e914192ac3d1d11ad7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 3 Jun 2020 09:12:12 -0600 Subject: [PATCH 43/54] remove extra Clear call --- .../Json/Converters/JsonNonStringKeyDictionaryConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs index f2ddd7fea2..636ef5372f 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -73,7 +73,6 @@ namespace MediaBrowser.Common.Json.Converters convertedDictionary[k?.ToString()] = v; } JsonSerializer.Serialize(writer, convertedDictionary, options); - convertedDictionary.Clear(); } } } From 3e749eabdf10f9a070a6c303ec37a912f9657e58 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 4 Jun 2020 07:29:00 -0600 Subject: [PATCH 44/54] Fix doc errors --- Jellyfin.Api/Controllers/DevicesController.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index a46d3f9370..1e75579033 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,10 +1,8 @@ #nullable enable using System; -using System.Collections.Generic; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; @@ -45,8 +43,8 @@ namespace Jellyfin.Api.Controllers /// /// Get Devices. /// - /// /// Gets or sets a value indicating whether [supports synchronize]. - /// /// Gets or sets the user identifier. + /// Gets or sets a value indicating whether [supports synchronize]. + /// Gets or sets the user identifier. /// Devices retrieved. /// An containing the list of devices. [HttpGet] From 598cd94c4d5fd8911c9e30af7be47ca6eac7c975 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 6 Jun 2020 16:51:21 -0600 Subject: [PATCH 45/54] Add CSS output formatter. --- .../ApiServiceCollectionExtensions.cs | 2 + .../Formatters/CssOutputFormatter.cs | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Jellyfin.Server/Formatters/CssOutputFormatter.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index cb4189587d..1c7fcbc066 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -77,6 +77,8 @@ namespace Jellyfin.Server.Extensions opts.UseGeneralRoutePrefix(baseUrl); opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); + + opts.OutputFormatters.Add(new CssOutputFormatter()); }) // Clear app parts to avoid other assemblies being picked up diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs new file mode 100644 index 0000000000..b26e7f96a0 --- /dev/null +++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs @@ -0,0 +1,37 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Jellyfin.Server.Formatters +{ + /// + /// Css output formatter. + /// + public class CssOutputFormatter : TextOutputFormatter + { + /// + /// Initializes a new instance of the class. + /// + public CssOutputFormatter() + { + SupportedMediaTypes.Clear(); + SupportedMediaTypes.Add("text/css"); + + SupportedEncodings.Add(Encoding.UTF8); + SupportedEncodings.Add(Encoding.Unicode); + } + + /// + /// Write context object to stream. + /// + /// Writer context. + /// Unused. Writer encoding. + /// Write stream task. + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + return context.HttpContext.Response.WriteAsync(context.Object?.ToString()); + } + } +} \ No newline at end of file From 04abb281c0905f1dbd21365d98756790dbb30973 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 6 Jun 2020 16:52:23 -0600 Subject: [PATCH 46/54] Add CSS output formatter. --- Jellyfin.Server/Formatters/CssOutputFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs index b26e7f96a0..1dbddc79a8 100644 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs @@ -34,4 +34,4 @@ namespace Jellyfin.Server.Formatters return context.HttpContext.Response.WriteAsync(context.Object?.ToString()); } } -} \ No newline at end of file +} From 48cbac934ba593fc5f4fed0eb0db81061eb4a787 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 6 Jun 2020 16:53:49 -0600 Subject: [PATCH 47/54] Don't clear media types --- Jellyfin.Server/Formatters/CssOutputFormatter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs index 1dbddc79a8..b3771b7fe6 100644 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs @@ -16,7 +16,6 @@ namespace Jellyfin.Server.Formatters /// public CssOutputFormatter() { - SupportedMediaTypes.Clear(); SupportedMediaTypes.Add("text/css"); SupportedEncodings.Add(Encoding.UTF8); From f3029428cd118b34d73706fdd85cc3918b312a86 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 7 Jun 2020 15:33:54 +0200 Subject: [PATCH 48/54] Fix suggestions --- Jellyfin.Api/Controllers/SearchController.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 15a650bf97..2ee3ef25b3 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -10,9 +10,9 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Search; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers /// Search controller. /// [Route("/Search/Hints")] - [Authenticated] + [Authorize] public class SearchController : BaseJellyfinApiController { private readonly ISearchEngine _searchEngine; @@ -52,9 +52,9 @@ namespace Jellyfin.Api.Controllers /// /// Gets the search hint result. /// - /// The record index to start at. All items with a lower index will be dropped from the results. - /// The maximum number of records to return. - /// Supply a user id to search within a user's library or omit to search all. + /// 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. Supply a user id to search within a user's library or omit to search all. /// The search term to filter on. /// Optional filter whether to include people. /// Optional filter whether to include media. @@ -117,11 +117,11 @@ namespace Jellyfin.Api.Controllers IsSports = isSports }); - return Ok(new SearchHintResult + return new SearchHintResult { TotalRecordCount = result.TotalRecordCount, SearchHints = result.Items.Select(GetSearchHintResult).ToArray() - }); + }; } /// From 7fa374f8a2560cd1c58584b3d5b0567c91ef4138 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 7 Jun 2020 15:41:49 +0200 Subject: [PATCH 49/54] Move Split method from BaseJellyfinApiController.cs to RequestHelpers.cs --- Jellyfin.Api/BaseJellyfinApiController.cs | 18 ------------ Jellyfin.Api/Controllers/SearchController.cs | 7 +++-- Jellyfin.Api/Helpers/RequestHelpers.cs | 29 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 Jellyfin.Api/Helpers/RequestHelpers.cs diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 6a9e48f8dd..22b9c3fd67 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -10,23 +10,5 @@ namespace Jellyfin.Api [Route("[controller]")] public class BaseJellyfinApiController : ControllerBase { - /// - /// Splits a string at a seperating character into an array of substrings. - /// - /// The string to split. - /// The char that seperates the substrings. - /// Option to remove empty substrings from the array. - /// An array of the substrings. - internal static string[] Split(string value, char separator, bool removeEmpty) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - return removeEmpty - ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) - : value.Split(separator); - } } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 2ee3ef25b3..b6178e121d 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -105,9 +106,9 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId, - IncludeItemTypes = Split(includeItemTypes, ',', true), - ExcludeItemTypes = Split(excludeItemTypes, ',', true), - MediaTypes = Split(mediaTypes, ',', true), + IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), ParentId = parentId, IsKids = isKids, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs new file mode 100644 index 0000000000..6c661e2d32 --- /dev/null +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -0,0 +1,29 @@ +using System; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Request Extensions. + /// + public static class RequestHelpers + { + /// + /// Splits a string at a seperating character into an array of substrings. + /// + /// The string to split. + /// The char that seperates the substrings. + /// Option to remove empty substrings from the array. + /// An array of the substrings. + internal static string[] Split(string value, char separator, bool removeEmpty) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty(); + } + + return removeEmpty + ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) + : value.Split(separator); + } + } +} From cefa9d3c086fd01b6f05080fa272fcf1f76158f2 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 7 Jun 2020 18:10:08 +0200 Subject: [PATCH 50/54] Add default values for parameters and fix spelling --- Jellyfin.Api/Controllers/SearchController.cs | 22 ++++++++++---------- Jellyfin.Api/Helpers/RequestHelpers.cs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index b6178e121d..411c19a59b 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -57,11 +57,6 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Optional. Supply a user id to search within a user's library or omit to search all. /// The search term to filter on. - /// Optional filter whether to include people. - /// Optional filter whether to include media. - /// Optional filter whether to include genres. - /// Optional filter whether to include studios. - /// Optional filter whether to include artists. /// If specified, only results with the specified item types are returned. This allows multiple, comma delimeted. /// If specified, results with these item types are filtered out. This allows multiple, comma delimeted. /// If specified, only results with the specified media types are returned. This allows multiple, comma delimeted. @@ -71,6 +66,11 @@ namespace Jellyfin.Api.Controllers /// Optional filter for news. /// Optional filter for kids. /// Optional filter for sports. + /// Optional filter whether to include people. + /// Optional filter whether to include media. + /// Optional filter whether to include genres. + /// Optional filter whether to include studios. + /// Optional filter whether to include artists. /// An with the results of the search. [HttpGet] [Description("Gets search hints based on a search term")] @@ -80,11 +80,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] Guid userId, [FromQuery, Required] string searchTerm, - [FromQuery] bool includePeople, - [FromQuery] bool includeMedia, - [FromQuery] bool includeGenres, - [FromQuery] bool includeStudios, - [FromQuery] bool includeArtists, [FromQuery] string includeItemTypes, [FromQuery] string excludeItemTypes, [FromQuery] string mediaTypes, @@ -93,7 +88,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isSeries, [FromQuery] bool? isNews, [FromQuery] bool? isKids, - [FromQuery] bool? isSports) + [FromQuery] bool? isSports, + [FromQuery] bool includePeople = true, + [FromQuery] bool includeMedia = true, + [FromQuery] bool includeGenres = true, + [FromQuery] bool includeStudios = true, + [FromQuery] bool includeArtists = true) { var result = _searchEngine.GetSearchHints(new SearchQuery { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 6c661e2d32..9f4d34f9c6 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -8,10 +8,10 @@ namespace Jellyfin.Api.Helpers public static class RequestHelpers { /// - /// Splits a string at a seperating character into an array of substrings. + /// Splits a string at a separating character into an array of substrings. /// /// The string to split. - /// The char that seperates the substrings. + /// The char that separates the substrings. /// Option to remove empty substrings from the array. /// An array of the substrings. internal static string[] Split(string value, char separator, bool removeEmpty) From 1491e93d841a8c40ea1a9fed9d99427a64ccd66c Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 9 Jun 2020 11:00:37 +0200 Subject: [PATCH 51/54] Add response code documentation --- Jellyfin.Api/Controllers/SearchController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 411c19a59b..ec05e4fb4f 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -71,6 +71,7 @@ namespace Jellyfin.Api.Controllers /// Optional filter whether to include genres. /// Optional filter whether to include studios. /// Optional filter whether to include artists. + /// Search hint returned. /// An with the results of the search. [HttpGet] [Description("Gets search hints based on a search term")] From dc190e56833235c1b20fa76b8382da80fc62b0b7 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 9 Jun 2020 18:56:17 +0200 Subject: [PATCH 52/54] Move ActivityLogService to Jellyfin.Api --- .../System/ActivityLogController.cs | 54 ++++++++++++++++ MediaBrowser.Api/System/ActivityLogService.cs | 61 ------------------- 2 files changed, 54 insertions(+), 61 deletions(-) create mode 100644 Jellyfin.Api/Controllers/System/ActivityLogController.cs delete mode 100644 MediaBrowser.Api/System/ActivityLogService.cs diff --git a/Jellyfin.Api/Controllers/System/ActivityLogController.cs b/Jellyfin.Api/Controllers/System/ActivityLogController.cs new file mode 100644 index 0000000000..f1daed2edd --- /dev/null +++ b/Jellyfin.Api/Controllers/System/ActivityLogController.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using Jellyfin.Api.Constants; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers.System +{ + /// + /// Activity log controller. + /// + [Route("/System/ActivityLog/Entries")] + [Authorize(Policy = Policies.RequiresElevation)] + public class ActivityLogController : BaseJellyfinApiController + { + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + public ActivityLogController(IActivityManager activityManager) + { + _activityManager = activityManager; + } + + /// + /// Gets activity log entries. + /// + /// 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. The minimum date. Format = ISO. + /// Optional. Only returns activities that have a user associated. + /// Activity log returned. + /// A containing the log entries. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLogEntries( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string minDate, + bool? hasUserId) + { + DateTime? startDate = string.IsNullOrWhiteSpace(minDate) ? + (DateTime?)null : + DateTime.Parse(minDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + + return _activityManager.GetActivityLogEntries(startDate, hasUserId, startIndex, limit); + } + } +} diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs deleted file mode 100644 index f95fa7ca0b..0000000000 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Globalization; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.System -{ - [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")] - public class GetActivityLogs : IReturn> - { - /// - /// Skips over a given number of items within the results. Use for paging. - /// - /// The start index. - [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? StartIndex { get; set; } - - /// - /// The maximum number of items to return - /// - /// The limit. - [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Limit { get; set; } - - [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string MinDate { get; set; } - - public bool? HasUserId { get; set; } - } - - [Authenticated(Roles = "Admin")] - public class ActivityLogService : BaseApiService - { - private readonly IActivityManager _activityManager; - - public ActivityLogService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IActivityManager activityManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _activityManager = activityManager; - } - - public object Get(GetActivityLogs request) - { - DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? - (DateTime?)null : - DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - - var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); - - return ToOptimizedResult(result); - } - } -} From 1bf6c085eda59034687f24fa5b5997389aede9e5 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 13:09:23 +0200 Subject: [PATCH 53/54] Move File; Move Route; use DateTime? in Query --- .../{System => }/ActivityLogController.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) rename Jellyfin.Api/Controllers/{System => }/ActivityLogController.cs (80%) diff --git a/Jellyfin.Api/Controllers/System/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs similarity index 80% rename from Jellyfin.Api/Controllers/System/ActivityLogController.cs rename to Jellyfin.Api/Controllers/ActivityLogController.cs index f1daed2edd..8d37a83738 100644 --- a/Jellyfin.Api/Controllers/System/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -7,12 +7,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers.System +namespace Jellyfin.Api.Controllers { /// /// Activity log controller. /// - [Route("/System/ActivityLog/Entries")] + [Route("/System/ActivityLog")] [Authorize(Policy = Policies.RequiresElevation)] public class ActivityLogController : BaseJellyfinApiController { @@ -36,19 +36,15 @@ namespace Jellyfin.Api.Controllers.System /// Optional. Only returns activities that have a user associated. /// Activity log returned. /// A containing the log entries. - [HttpGet] + [HttpGet("Entries")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLogEntries( [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string minDate, + [FromQuery] DateTime? minDate, bool? hasUserId) { - DateTime? startDate = string.IsNullOrWhiteSpace(minDate) ? - (DateTime?)null : - DateTime.Parse(minDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - - return _activityManager.GetActivityLogEntries(startDate, hasUserId, startIndex, limit); + return _activityManager.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); } } } From 6a70081643de80a2053b5903644cdcfa4bcbfc61 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 15:57:31 +0200 Subject: [PATCH 54/54] Move ApiKeyService to Jellyfin.Api --- Jellyfin.Api/Controllers/ApiKeyController.cs | 97 ++++++++++++++++++++ MediaBrowser.Api/Sessions/ApiKeyService.cs | 85 ----------------- 2 files changed, 97 insertions(+), 85 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ApiKeyController.cs delete mode 100644 MediaBrowser.Api/Sessions/ApiKeyService.cs diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs new file mode 100644 index 0000000000..ed521c1fc5 --- /dev/null +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -0,0 +1,97 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Jellyfin.Api.Constants; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Authentication controller. + /// + [Route("/Auth")] + public class ApiKeyController : BaseJellyfinApiController + { + private readonly ISessionManager _sessionManager; + private readonly IServerApplicationHost _appHost; + private readonly IAuthenticationRepository _authRepo; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public ApiKeyController( + ISessionManager sessionManager, + IServerApplicationHost appHost, + IAuthenticationRepository authRepo) + { + _sessionManager = sessionManager; + _appHost = appHost; + _authRepo = authRepo; + } + + /// + /// Get all keys. + /// + /// Api keys retrieved. + /// A with all keys. + [HttpGet("Keys")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetKeys() + { + var result = _authRepo.Get(new AuthenticationInfoQuery + { + HasUser = false + }); + + return result; + } + + /// + /// Create a new api key. + /// + /// Name of the app using the authentication key. + /// Api key created. + /// A . + [HttpPost("Keys")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult CreateKey([FromQuery, Required] string app) + { + _authRepo.Create(new AuthenticationInfo + { + AppName = app, + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString + }); + return NoContent(); + } + + /// + /// Remove an api key. + /// + /// The access token to delete. + /// Api key deleted. + /// A . + [HttpDelete("Keys/{key}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RevokeKey([FromRoute] string key) + { + _sessionManager.RevokeToken(key); + return NoContent(); + } + } +} diff --git a/MediaBrowser.Api/Sessions/ApiKeyService.cs b/MediaBrowser.Api/Sessions/ApiKeyService.cs deleted file mode 100644 index 5102ce0a7c..0000000000 --- a/MediaBrowser.Api/Sessions/ApiKeyService.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Globalization; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Sessions -{ - [Route("/Auth/Keys", "GET")] - [Authenticated(Roles = "Admin")] - public class GetKeys - { - } - - [Route("/Auth/Keys/{Key}", "DELETE")] - [Authenticated(Roles = "Admin")] - public class RevokeKey - { - [ApiMember(Name = "Key", Description = "Authentication key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Key { get; set; } - } - - [Route("/Auth/Keys", "POST")] - [Authenticated(Roles = "Admin")] - public class CreateKey - { - [ApiMember(Name = "App", Description = "Name of the app using the authentication key", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string App { get; set; } - } - - public class ApiKeyService : BaseApiService - { - private readonly ISessionManager _sessionManager; - - private readonly IAuthenticationRepository _authRepo; - - private readonly IServerApplicationHost _appHost; - - public ApiKeyService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionManager sessionManager, - IServerApplicationHost appHost, - IAuthenticationRepository authRepo) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionManager = sessionManager; - _authRepo = authRepo; - _appHost = appHost; - } - - public void Delete(RevokeKey request) - { - _sessionManager.RevokeToken(request.Key); - } - - public void Post(CreateKey request) - { - _authRepo.Create(new AuthenticationInfo - { - AppName = request.App, - AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - DateCreated = DateTime.UtcNow, - DeviceId = _appHost.SystemId, - DeviceName = _appHost.FriendlyName, - AppVersion = _appHost.ApplicationVersionString - }); - } - - public object Get(GetKeys request) - { - var result = _authRepo.Get(new AuthenticationInfoQuery - { - HasUser = false - }); - - return result; - } - } -}