Merge remote-tracking branch 'upstream/api-migration' into api-swagger-auth
This commit is contained in:
commit
2fe65d99f6
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,13 +37,20 @@ namespace Jellyfin.Api.Auth
|
|||
/// <inheritdoc />
|
||||
protected override Task<AuthenticateResult> 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[]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api
|
||||
|
|
50
Jellyfin.Api/Controllers/ActivityLogController.cs
Normal file
50
Jellyfin.Api/Controllers/ActivityLogController.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Activity log controller.
|
||||
/// </summary>
|
||||
[Route("/System/ActivityLog")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class ActivityLogController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
||||
public ActivityLogController(IActivityManager activityManager)
|
||||
{
|
||||
_activityManager = activityManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets activity log entries.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
|
||||
/// <param name="hasUserId">Optional. Only returns activities that have a user associated.</param>
|
||||
/// <response code="200">Activity log returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
||||
[HttpGet("Entries")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] DateTime? minDate,
|
||||
bool? hasUserId)
|
||||
{
|
||||
return _activityManager.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
|
||||
}
|
||||
}
|
||||
}
|
97
Jellyfin.Api/Controllers/ApiKeyController.cs
Normal file
97
Jellyfin.Api/Controllers/ApiKeyController.cs
Normal file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication controller.
|
||||
/// </summary>
|
||||
[Route("/Auth")]
|
||||
public class ApiKeyController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
public ApiKeyController(
|
||||
ISessionManager sessionManager,
|
||||
IServerApplicationHost appHost,
|
||||
IAuthenticationRepository authRepo)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_appHost = appHost;
|
||||
_authRepo = authRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all keys.
|
||||
/// </summary>
|
||||
/// <response code="200">Api keys retrieved.</response>
|
||||
/// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
|
||||
[HttpGet("Keys")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
HasUser = false
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new api key.
|
||||
/// </summary>
|
||||
/// <param name="app">Name of the app using the authentication key.</param>
|
||||
/// <response code="204">Api key created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an api key.
|
||||
/// </summary>
|
||||
/// <param name="key">The access token to delete.</param>
|
||||
/// <response code="204">Api key deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Keys/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RevokeKey([FromRoute] string key)
|
||||
{
|
||||
_sessionManager.RevokeToken(key);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="jsonSerializer">Instance of the <see cref="IJsonSerializer"/> interface.</param>
|
||||
public ConfigurationController(
|
||||
IServerConfigurationManager configurationManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IJsonSerializer jsonSerializer)
|
||||
IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers
|
|||
public async Task<ActionResult> 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();
|
||||
}
|
||||
|
|
156
Jellyfin.Api/Controllers/DevicesController.cs
Normal file
156
Jellyfin.Api/Controllers/DevicesController.cs
Normal file
|
@ -0,0 +1,156 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
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;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Devices Controller.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
public DevicesController(
|
||||
IDeviceManager deviceManager,
|
||||
IAuthenticationRepository authenticationRepository,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_deviceManager = deviceManager;
|
||||
_authenticationRepository = authenticationRepository;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Devices.
|
||||
/// </summary>
|
||||
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
||||
/// <param name="userId">Gets or sets the user identifier.</param>
|
||||
/// <response code="200">Devices retrieved.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||
{
|
||||
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
|
||||
return _deviceManager.GetDevices(deviceQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get info for a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="200">Device info retrieved.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Info")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDevice(id);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get options for a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="200">Device options retrieved.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update device options.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <param name="deviceOptions">Device Options.</param>
|
||||
/// <response code="200">Device options updated.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpPost("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateDeviceOptions(
|
||||
[FromQuery, BindRequired] string id,
|
||||
[FromBody, BindRequired] DeviceOptions deviceOptions)
|
||||
{
|
||||
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
|
||||
if (existingDeviceOptions == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="200">Device deleted.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[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)
|
||||
{
|
||||
_sessionManager.Logout(session);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
120
Jellyfin.Api/Controllers/PackageController.cs
Normal file
120
Jellyfin.Api/Controllers/PackageController.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
#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
|
||||
{
|
||||
/// <summary>
|
||||
/// Package Controller.
|
||||
/// </summary>
|
||||
[Route("Packages")]
|
||||
[Authorize]
|
||||
public class PackageController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PackageController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>Installation Manager.</param>
|
||||
public PackageController(IInstallationManager installationManager)
|
||||
{
|
||||
_installationManager = installationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a package by name or assembly GUID.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the package.</param>
|
||||
/// <param name="assemblyGuid">The GUID of the associated assembly.</param>
|
||||
/// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
|
||||
[HttpGet("/{Name}")]
|
||||
[ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
|
||||
[FromRoute] [Required] string name,
|
||||
[FromQuery] string? assemblyGuid)
|
||||
{
|
||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
var result = _installationManager.FilterPackages(
|
||||
packages,
|
||||
name,
|
||||
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available packages.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
|
||||
public async Task<IEnumerable<PackageInfo>> GetPackages()
|
||||
{
|
||||
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a package.
|
||||
/// </summary>
|
||||
/// <param name="name">Package name.</param>
|
||||
/// <param name="assemblyGuid">GUID of the associated assembly.</param>
|
||||
/// <param name="version">Optional version. Defaults to latest version.</param>
|
||||
/// <response code="200">Package found.</response>
|
||||
/// <response code="404">Package not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
|
||||
[HttpPost("/Installed/{Name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a package installation.
|
||||
/// </summary>
|
||||
/// <param name="id">Installation Id.</param>
|
||||
/// <response code="200">Installation cancelled.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns>
|
||||
[HttpDelete("/Installing/{id}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public IActionResult CancelPackageInstallation(
|
||||
[FromRoute] [Required] string id)
|
||||
{
|
||||
_installationManager.CancelInstallation(new Guid(id));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
268
Jellyfin.Api/Controllers/SearchController.cs
Normal file
268
Jellyfin.Api/Controllers/SearchController.cs
Normal file
|
@ -0,0 +1,268 @@
|
|||
using System;
|
||||
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;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Search controller.
|
||||
/// </summary>
|
||||
[Route("/Search/Hints")]
|
||||
[Authorize]
|
||||
public class SearchController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISearchEngine _searchEngine;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
|
||||
public SearchController(
|
||||
ISearchEngine searchEngine,
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService,
|
||||
IImageProcessor imageProcessor)
|
||||
{
|
||||
_searchEngine = searchEngine;
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hint result.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
|
||||
/// <param name="searchTerm">The search term to filter on.</param>
|
||||
/// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="parentId">If specified, only children of the parent are returned.</param>
|
||||
/// <param name="isMovie">Optional filter for movies.</param>
|
||||
/// <param name="isSeries">Optional filter for series.</param>
|
||||
/// <param name="isNews">Optional filter for news.</param>
|
||||
/// <param name="isKids">Optional filter for kids.</param>
|
||||
/// <param name="isSports">Optional filter for sports.</param>
|
||||
/// <param name="includePeople">Optional filter whether to include people.</param>
|
||||
/// <param name="includeMedia">Optional filter whether to include media.</param>
|
||||
/// <param name="includeGenres">Optional filter whether to include genres.</param>
|
||||
/// <param name="includeStudios">Optional filter whether to include studios.</param>
|
||||
/// <param name="includeArtists">Optional filter whether to include artists.</param>
|
||||
/// <response code="200">Search hint returned.</response>
|
||||
/// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
|
||||
[HttpGet]
|
||||
[Description("Gets search hints based on a search term")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<SearchHintResult> Get(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] Guid userId,
|
||||
[FromQuery, Required] string searchTerm,
|
||||
[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,
|
||||
[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
|
||||
{
|
||||
Limit = limit,
|
||||
SearchTerm = searchTerm,
|
||||
IncludeArtists = includeArtists,
|
||||
IncludeGenres = includeGenres,
|
||||
IncludeMedia = includeMedia,
|
||||
IncludePeople = includePeople,
|
||||
IncludeStudios = includeStudios,
|
||||
StartIndex = startIndex,
|
||||
UserId = userId,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
ParentId = parentId,
|
||||
|
||||
IsKids = isKids,
|
||||
IsMovie = isMovie,
|
||||
IsNews = isNews,
|
||||
IsSeries = isSeries,
|
||||
IsSports = isSports
|
||||
});
|
||||
|
||||
return new SearchHintResult
|
||||
{
|
||||
TotalRecordCount = result.TotalRecordCount,
|
||||
SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hint result.
|
||||
/// </summary>
|
||||
/// <param name="hintInfo">The hint info.</param>
|
||||
/// <returns>SearchHintResult.</returns>
|
||||
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<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage == null)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<BaseItem>(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<BaseItem>(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<T>(BaseItem item, ImageType type)
|
||||
where T : BaseItem
|
||||
{
|
||||
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,6 +109,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="200">Initial user retrieved.</response>
|
||||
/// <returns>The first user.</returns>
|
||||
[HttpGet("User")]
|
||||
[HttpGet("FirstUser")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<StartupUserDto> GetFirstUser()
|
||||
{
|
||||
|
|
84
Jellyfin.Api/Controllers/VideoAttachmentsController.cs
Normal file
84
Jellyfin.Api/Controllers/VideoAttachmentsController.cs
Normal file
|
@ -0,0 +1,84 @@
|
|||
#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 Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Attachments controller.
|
||||
/// </summary>
|
||||
[Route("Videos")]
|
||||
[Authorize]
|
||||
public class VideoAttachmentsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IAttachmentExtractor _attachmentExtractor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
|
||||
public VideoAttachmentsController(
|
||||
ILibraryManager libraryManager,
|
||||
IAttachmentExtractor attachmentExtractor)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_attachmentExtractor = attachmentExtractor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get video attachment.
|
||||
/// </summary>
|
||||
/// <param name="videoId">Video ID.</param>
|
||||
/// <param name="mediaSourceId">Media Source ID.</param>
|
||||
/// <param name="index">Attachment Index.</param>
|
||||
/// <response code="200">Attachment retrieved.</response>
|
||||
/// <response code="404">Video or attachment not found.</response>
|
||||
/// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
|
||||
[HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<FileStreamResult>> 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 = string.IsNullOrWhiteSpace(attachment.MimeType)
|
||||
? MediaTypeNames.Application.Octet
|
||||
: attachment.MimeType;
|
||||
|
||||
return new FileStreamResult(stream, contentType);
|
||||
}
|
||||
catch (ResourceNotFoundException e)
|
||||
{
|
||||
return NotFound(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
Jellyfin.Api/Helpers/RequestHelpers.cs
Normal file
29
Jellyfin.Api/Helpers/RequestHelpers.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Request Extensions.
|
||||
/// </summary>
|
||||
public static class RequestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Splits a string at a separating character into an array of substrings.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to split.</param>
|
||||
/// <param name="separator">The char that separates the substrings.</param>
|
||||
/// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
|
||||
/// <returns>An array of the substrings.</returns>
|
||||
internal static string[] Split(string value, char separator, bool removeEmpty)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return removeEmpty
|
||||
? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
|
||||
: value.Split(separator);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ 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 +10,9 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
|||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Server.Formatters;
|
||||
using Jellyfin.Server.Models;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -71,11 +73,18 @@ namespace Jellyfin.Server.Extensions
|
|||
/// <returns>The MVC builder.</returns>
|
||||
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());
|
||||
opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
|
||||
|
||||
opts.OutputFormatters.Add(new CssOutputFormatter());
|
||||
})
|
||||
|
||||
// Clear app parts to avoid other assemblies being picked up
|
||||
|
@ -83,8 +92,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();
|
||||
}
|
||||
|
@ -118,6 +139,7 @@ namespace Jellyfin.Server.Extensions
|
|||
{
|
||||
{ securitySchemeRef, Array.Empty<string>() }
|
||||
});
|
||||
c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
|
||||
|
||||
// Add all xml doc files to swagger generator.
|
||||
var xmlFiles = Directory.GetFiles(
|
||||
|
@ -137,7 +159,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<Dictionary<ImageType, string>>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Properties = typeof(ImageType).GetEnumNames().ToDictionary(
|
||||
name => name,
|
||||
name => new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "string"
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
|
||||
/// </summary>
|
||||
public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase)
|
||||
public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase)
|
||||
{
|
||||
SupportedMediaTypes.Clear();
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
|
||||
|
|
36
Jellyfin.Server/Formatters/CssOutputFormatter.cs
Normal file
36
Jellyfin.Server/Formatters/CssOutputFormatter.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Jellyfin.Server.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// Css output formatter.
|
||||
/// </summary>
|
||||
public class CssOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
|
||||
/// </summary>
|
||||
public CssOutputFormatter()
|
||||
{
|
||||
SupportedMediaTypes.Add("text/css");
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write context object to stream.
|
||||
/// </summary>
|
||||
/// <param name="context">Writer context.</param>
|
||||
/// <param name="selectedEncoding">Unused. Writer encoding.</param>
|
||||
/// <returns>Write stream task.</returns>
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
|
||||
/// </summary>
|
||||
public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase)
|
||||
public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase)
|
||||
{
|
||||
SupportedMediaTypes.Clear();
|
||||
// Add application/json for default formatter
|
||||
|
|
|
@ -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<ExceptionMiddleware> _logger;
|
||||
private readonly IServerConfigurationManager _configuration;
|
||||
private readonly IWebHostEnvironment _hostEnvironment;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
|
||||
|
@ -27,14 +30,17 @@ namespace Jellyfin.Server.Middleware
|
|||
/// <param name="next">Next request delegate.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param>
|
||||
public ExceptionMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ExceptionMiddleware> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IWebHostEnvironment hostEnvironment)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_configuration = serverConfigurationManager;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -85,7 +91,11 @@ namespace Jellyfin.Server.Middleware
|
|||
|
||||
context.Response.StatusCode = GetStatusCode(ex);
|
||||
context.Response.ContentType = MediaTypeNames.Text.Plain;
|
||||
var errorContent = NormalizeExceptionMessage(ex.Message);
|
||||
|
||||
// Don't send exception unless the server is in a Development environment
|
||||
var errorContent = _hostEnvironment.IsDevelopment()
|
||||
? NormalizeExceptionMessage(ex.Message)
|
||||
: "Error processing request.";
|
||||
await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Jellyfin.Server.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Json Options.
|
||||
/// </summary>
|
||||
public static class JsonOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets CamelCase json options.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions CamelCase
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = DefaultJsonOptions;
|
||||
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets PascalCase json options.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions PascalCase
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = DefaultJsonOptions;
|
||||
options.PropertyNamingPolicy = null;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets base Json Serializer Options.
|
||||
/// </summary>
|
||||
private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions();
|
||||
}
|
||||
}
|
30
Jellyfin.Server/Models/ServerCorsPolicy.cs
Normal file
30
Jellyfin.Server/Models/ServerCorsPolicy.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
|
||||
namespace Jellyfin.Server.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Server Cors Policy.
|
||||
/// </summary>
|
||||
public static class ServerCorsPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Default policy name.
|
||||
/// </summary>
|
||||
public const string DefaultPolicyName = "DefaultCorsPolicy";
|
||||
|
||||
/// <summary>
|
||||
/// Default Policy. Allow Everything.
|
||||
/// </summary>
|
||||
public static readonly CorsPolicy DefaultPolicy = new CorsPolicy
|
||||
{
|
||||
// Allow any origin
|
||||
Origins = { "*" },
|
||||
|
||||
// Allow any method
|
||||
Methods = { "*" },
|
||||
|
||||
// Allow any header
|
||||
Headers = { "*" }
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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<WebSocketMiddleware>();
|
||||
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 =>
|
||||
{
|
||||
|
|
|
@ -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<AttachmentService> logger,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IAttachmentExtractor attachmentExtractor)
|
||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_attachmentExtractor = attachmentExtractor;
|
||||
}
|
||||
|
||||
public async Task<object> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<QueryResult<DeviceInfo>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Devices/Info", "GET", Summary = "Gets info for a device")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class GetDeviceInfo : IReturn<DeviceInfo>
|
||||
{
|
||||
[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<DeviceOptions>
|
||||
{
|
||||
[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<ContentUploadHistory>
|
||||
{
|
||||
[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<DeviceService> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,10 @@
|
|||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Attachments" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetPackage
|
||||
/// </summary>
|
||||
[Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")]
|
||||
[Authenticated]
|
||||
public class GetPackage : IReturn<PackageInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
[ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
[ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string AssemblyGuid { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class GetPackages
|
||||
/// </summary>
|
||||
[Route("/Packages", "GET", Summary = "Gets available packages")]
|
||||
[Authenticated]
|
||||
public class GetPackages : IReturn<PackageInfo[]>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class InstallPackage
|
||||
/// </summary>
|
||||
[Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class InstallPackage : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
[ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
[ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string AssemblyGuid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the version.
|
||||
/// </summary>
|
||||
/// <value>The version.</value>
|
||||
[ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Version { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class CancelPackageInstallation
|
||||
/// </summary>
|
||||
[Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class CancelPackageInstallation : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class PackageService
|
||||
/// </summary>
|
||||
public class PackageService : BaseApiService
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
||||
public PackageService(
|
||||
ILogger<PackageService> logger,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
IInstallationManager installationManager)
|
||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
||||
{
|
||||
_installationManager = installationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public async Task<object> Get(GetPackages request)
|
||||
{
|
||||
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(packages.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <exception cref="ResourceNotFoundException"></exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(CancelPackageInstallation request)
|
||||
{
|
||||
_installationManager.CancelInstallation(new Guid(request.Id));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetSearchHints
|
||||
/// </summary>
|
||||
[Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")]
|
||||
public class GetSearchHints : IReturn<SearchHintResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips over a given number of items within the results. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Search characters used to find items
|
||||
/// </summary>
|
||||
/// <value>The index by.</value>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class SearchService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class SearchService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// The _search engine
|
||||
/// </summary>
|
||||
private readonly ISearchEngine _searchEngine;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="searchEngine">The search engine.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="dtoService">The dto service.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
public SearchService(
|
||||
ILogger<SearchService> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetSearchHints request)
|
||||
{
|
||||
var result = GetSearchHintsAsync(request);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hints async.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task{IEnumerable{SearchHintResult}}.</returns>
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hint result.
|
||||
/// </summary>
|
||||
/// <param name="hintInfo">The hint info.</param>
|
||||
/// <returns>SearchHintResult.</returns>
|
||||
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<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage == null)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<BaseItem>(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<BaseItem>(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<T>(BaseItem item, ImageType type)
|
||||
where T : BaseItem
|
||||
{
|
||||
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ApiKeyService> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<QueryResult<ActivityLogEntry>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips over a given number of items within the results. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
[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<ActivityLogService> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
#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
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for Dictionaries without string key.
|
||||
/// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">Type of key.</typeparam>
|
||||
/// <typeparam name="TValue">Type of value.</typeparam>
|
||||
internal sealed class JsonNonStringKeyDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Read JSON.
|
||||
/// </summary>
|
||||
/// <param name="reader">The Utf8JsonReader.</param>
|
||||
/// <param name="typeToConvert">The type to convert.</param>
|
||||
/// <param name="options">The json serializer options.</param>
|
||||
/// <returns>Typed dictionary.</returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public override IDictionary<TKey, TValue> 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<TKey, TValue>)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<TKey, TValue> is not supported.");
|
||||
}
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var element = (KeyValuePair<string?, TValue>)enumerator.Current;
|
||||
instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write dictionary as Json.
|
||||
/// </summary>
|
||||
/// <param name="writer">The Utf8JsonWriter.</param>
|
||||
/// <param name="value">The dictionary value.</param>
|
||||
/// <param name="options">The Json serializer options.</param>
|
||||
public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
|
||||
{
|
||||
var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
|
||||
foreach (var (k, v) in value)
|
||||
{
|
||||
convertedDictionary[k?.ToString()] = v;
|
||||
}
|
||||
JsonSerializer.Serialize(writer, convertedDictionary, options);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972.
|
||||
/// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
|
||||
/// </summary>
|
||||
internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Only convert objects that implement IDictionary and do not have string keys.
|
||||
/// </summary>
|
||||
/// <param name="typeToConvert">Type convert.</param>
|
||||
/// <returns>Conversion ability.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create converter for generic dictionary type.
|
||||
/// </summary>
|
||||
/// <param name="typeToConvert">Type to convert.</param>
|
||||
/// <param name="options">Json serializer options.</param>
|
||||
/// <returns>JsonConverter for given type.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json
|
|||
/// <summary>
|
||||
/// Gets the default <see cref="JsonSerializerOptions" /> options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When changing these options, update
|
||||
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
|
||||
/// -> AddJellyfinApi
|
||||
/// -> AddJsonOptions
|
||||
/// </remarks>
|
||||
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
|
||||
public static JsonSerializerOptions GetOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions()
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Disallow,
|
||||
WriteIndented = false
|
||||
|
@ -23,8 +29,35 @@ namespace MediaBrowser.Common.Json
|
|||
|
||||
options.Converters.Add(new JsonGuidConverter());
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets CamelCase json options.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions CamelCase
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = GetOptions();
|
||||
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets PascalCase json options.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions PascalCase
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = GetOptions();
|
||||
options.PropertyNamingPolicy = null;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user